Calendar
The .pl-c-calendar contract styles date grids, date picker popovers, date-range
fields, and time options without owning date logic. Porchlight provides the
surface, spacing, focus treatment, and visual state mapping; the application
owns date math, locale labels, roving tabindex, keyboard behavior, validation,
and persistence.
Use it when a SaaS, BPM, workflow, ticketing, or admin UI needs a consistent calendar shell but must keep business rules in the app layer.
Semantic HTML
<section class="pl-c-calendar" aria-labelledby="due-date-month">
<header class="pl-c-calendar__header">
<button
class="pl-c-calendar__nav-button"
type="button"
aria-label="Previous month"
>
‹
</button>
<h2 class="pl-c-calendar__heading" id="due-date-month">June 2026</h2>
<button
class="pl-c-calendar__nav-button"
type="button"
aria-label="Next month"
>
›
</button>
</header>
<div class="pl-c-calendar__weekdays" aria-hidden="true">
<span class="pl-c-calendar__weekday">Mon</span>
<span class="pl-c-calendar__weekday">Tue</span>
<span class="pl-c-calendar__weekday">Wed</span>
<span class="pl-c-calendar__weekday">Thu</span>
<span class="pl-c-calendar__weekday">Fri</span>
<span class="pl-c-calendar__weekday">Sat</span>
<span class="pl-c-calendar__weekday">Sun</span>
</div>
<div class="pl-c-calendar__grid" role="grid" aria-labelledby="due-date-month">
<div class="pl-c-calendar__row" role="row">
<button
class="pl-c-calendar__day"
type="button"
role="gridcell"
data-outside-month
>
<span class="pl-c-calendar__day-label">29</span>
</button>
<button
class="pl-c-calendar__day"
type="button"
role="gridcell"
aria-current="date"
>
<span class="pl-c-calendar__day-label">1</span>
</button>
<button
class="pl-c-calendar__day"
type="button"
role="gridcell"
aria-selected="true"
>
<span class="pl-c-calendar__day-label">2</span>
</button>
</div>
</div>
</section>
Day cells should be real <button> elements unless the date is purely static.
Applications may use role="grid" with role="gridcell" and roving
tabindex; when using grid roles, wrap each week in role="row". Apps may
also use a simpler button group when their accessibility model calls for it.
Porchlight does not enforce either pattern in CSS. The
.pl-c-calendar__day-label wrapper is optional, but recommended for range
calendars because it lets the range track paint behind selected start/end days.
Date Picker Field
Pair the calendar with .pl-c-field and the native Popover API for dependency-free
field disclosure. The visible field may be a native date input, a text input
with an ISO value, or a read-only display input backed by hidden form data.
<div class="pl-c-field pl-c-date-picker">
<label class="pl-c-field__label" for="case-due-date">Due date</label>
<div class="pl-c-date-picker__field">
<input
class="pl-c-field__control pl-c-date-picker__control"
id="case-due-date"
name="due_date"
inputmode="numeric"
autocomplete="off"
value="2026-06-18"
aria-describedby="case-due-date-help"
/>
<button
class="pl-c-button pl-c-date-picker__trigger"
type="button"
popovertarget="case-due-calendar"
aria-label="Choose due date"
>
Calendar
</button>
</div>
<span class="pl-c-field__hint" id="case-due-date-help"
>Local SLA timezone: America/Chicago.</span
>
<div class="pl-c-date-picker__popover" id="case-due-calendar" popover>
<section class="pl-c-calendar" aria-labelledby="case-due-month">
...
</section>
</div>
</div>
Set --pl-c-date-picker-anchor per instance when multiple picker popovers appear
on one page. The default anchor is fine for a single picker. The picker popover
uses the native top layer and an overlay z-index; it keeps a full month visible
on ordinary desktop viewports, then scrolls only when the viewport is too short.
Date Range
Use .pl-c-date-range around two explicit fields when the app needs a start/end
contract. A range calendar can mark start, end, and in-range days with
data-range-start, data-range-end, and data-in-range.
<div class="pl-c-date-range">
<div class="pl-c-date-range__fields">
<label class="pl-c-field">
<span class="pl-c-field__label">SLA starts</span>
<input class="pl-c-field__control" name="sla_start" value="2026-06-16" />
</label>
<label class="pl-c-field">
<span class="pl-c-field__label">SLA ends</span>
<input class="pl-c-field__control" name="sla_end" value="2026-06-20" />
</label>
</div>
<section class="pl-c-calendar" aria-labelledby="sla-month">
<h2 class="pl-c-calendar__heading" id="sla-month">June 2026</h2>
<div class="pl-c-calendar__grid" role="grid" aria-labelledby="sla-month">
<div class="pl-c-calendar__row" role="row">
<button
class="pl-c-calendar__day"
type="button"
role="gridcell"
data-range-start
aria-selected="true"
>
<span class="pl-c-calendar__day-label">16</span>
</button>
<button
class="pl-c-calendar__day"
type="button"
role="gridcell"
data-in-range
>
<span class="pl-c-calendar__day-label">17</span>
</button>
<button
class="pl-c-calendar__day"
type="button"
role="gridcell"
data-range-end
aria-selected="true"
>
<span class="pl-c-calendar__day-label">20</span>
</button>
</div>
</div>
</section>
</div>
Time Options
.pl-c-time-picker styles discrete time choices. Use real buttons, or radio inputs
with labels if your app needs native form selection semantics.
<div class="pl-c-time-picker" role="listbox" aria-label="Reminder time">
<div class="pl-c-time-picker__grid">
<button class="pl-c-time-picker__option" type="button" role="option">
08:30
</button>
<button
class="pl-c-time-picker__option"
type="button"
role="option"
aria-selected="true"
>
09:00
</button>
<button
class="pl-c-time-picker__option"
type="button"
role="option"
disabled
>
09:30
</button>
</div>
</div>
Class Contract
| Selector | Role |
|---|---|
.pl-c-calendar |
Calendar surface wrapper. |
.pl-c-calendar__header |
Month heading and navigation row. |
.pl-c-calendar__heading |
Current month/range title. |
.pl-c-calendar__nav |
Optional wrapper for previous/next controls. |
.pl-c-calendar__nav-button |
Month navigation button. |
.pl-c-calendar__weekdays |
Seven-column weekday label row. |
.pl-c-calendar__weekday |
Weekday label, usually aria-hidden. |
.pl-c-calendar__grid |
Seven-column date grid. |
.pl-c-calendar__row |
Optional ARIA row wrapper for one calendar week. |
.pl-c-calendar__day |
Individual date button or gridcell. |
.pl-c-calendar__day-label |
Optional day number layer for selected/range art. |
.pl-c-calendar__footer |
Optional shortcut/status/action row. |
.pl-c-calendar__shortcut-list |
Optional list of quick date commands. |
.pl-c-date-picker |
Field + popover picker wrapper. |
.pl-c-date-picker__field |
Input and trigger row. |
.pl-c-date-picker__control |
Native field control inside the picker row. |
.pl-c-date-picker__trigger |
Button that opens the calendar popover. |
.pl-c-date-picker__popover |
Native popover element containing the calendar. |
.pl-c-date-range |
Start/end field and calendar wrapper. |
.pl-c-date-range__fields |
Responsive two-field date range grid. |
.pl-c-time-picker |
Time option surface. |
.pl-c-time-picker__grid |
Responsive time option grid. |
.pl-c-time-picker__option |
Selectable time button. |
State Contract
| State | Attribute | Notes |
|---|---|---|
| today | aria-current="date" or data-today |
Use aria-current="date" when it reflects the real current date. |
| selected day | aria-selected="true" or data-selected |
App controls single-date selection. |
| disabled day | disabled, aria-disabled="true", or data-disabled |
Prefer native disabled for real buttons that cannot be chosen. |
| outside month | data-outside-month |
Muted visual state for leading/trailing dates. |
| range start | data-range-start |
Usually paired with aria-selected="true". |
| range end | data-range-end |
Usually paired with aria-selected="true". |
| in range | data-in-range |
Applies the connecting range fill. |
| invalid field | aria-invalid="true" on a nested field control |
Field hint inherits the danger tone unless explicitly toned. |
| selected time | aria-selected="true" or data-selected |
Works on .pl-c-time-picker__option. |
Accessibility Contract
Porchlight intentionally does not implement calendar interaction. Your app should provide:
- Localized month headings, weekday labels, date names, and time formats.
- Keyboard behavior for previous/next month, day movement, Home/End, PageUp, PageDown, selection, and dismissal if you expose a grid picker.
- Roving
tabindexor another documented focus model for the grid. aria-liveannouncements when changing months if the update is not obvious.- Validation timing and messaging via
aria-invalid,aria-describedby, androle="alert"where appropriate. - Time zone and cutoff rules for SLA, due-date, and business-calendar logic.
For purely native browser behavior, use <input type="date">,
<input type="datetime-local">, or <input type="time"> inside .pl-c-field
instead. Reach for .pl-c-calendar when the product needs a custom grid or range
surface.
SSR, Hypermedia, And Frameworks
Any renderer can emit this contract. Server-rendered pages, hypermedia
fragment swaps, web components, React, Vue, Svelte, Solid, and similar
component systems should all produce the same classes, ARIA, native controls,
and data-* state attributes. Porchlight selectors are framework-agnostic and
do not rely on runtime-specific attributes.
Limitations
This component does not calculate dates, enforce min/max constraints, format locale strings, trap focus, manage the Popover API from unsupported browsers, or serialize ranges. It is a CSS contract for modern application markup, not a date library.
Tokens Consumed
--pl-color-{accent,accent-text,border,danger-text,surface,surface-2,text,text-muted},
--pl-control-{block-size,border-width,padding-inline,radius},
--pl-focus-{color,offset,size}, --pl-font-weight-*,
--pl-radius-{sm,lg,xl}, --pl-shadow-2, and --pl-space-*.
Tokens Exposed
| Token | Default | Purpose |
|---|---|---|
--pl-c-calendar-cell-size |
2.5rem |
Minimum day-cell block size. |
--pl-c-calendar-gap |
--pl-space-1 |
Gap between grid cells. |
--pl-c-calendar-radius |
--pl-radius-lg |
Day-cell radius. |
--pl-c-calendar-range-bg |
accent tint | Fill behind in-range days. |
--pl-c-calendar-range-hover-bg |
stronger accent tint | Hover fill for in-range days. |
--pl-c-date-picker-anchor |
--pl-date-picker-anchor |
Anchor name for a picker popover. |
--pl-c-date-picker-popover-size |
min(100vi - 2rem, 23rem) |
Popover inline size. |
--pl-c-date-picker-popover-max-block |
min(34rem, calc(100dvb - 2rem)) |
Popover max block size. |