Component Porchlight CSS

Calendar

Date, date-range, and time picker contracts for app-owned calendar behavior.

52 components 51 stable 1 experimental

Status
experimental
Since
0.8.0

Command

Search Porchlight

Calendar

The .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="c-calendar" aria-labelledby="due-date-month">
  <header class="c-calendar__header">
    <button
      class="c-calendar__nav-button"
      type="button"
      aria-label="Previous month"
    >

    </button>
    <h2 class="c-calendar__heading" id="due-date-month">June 2026</h2>
    <button
      class="c-calendar__nav-button"
      type="button"
      aria-label="Next month"
    >

    </button>
  </header>

  <div class="c-calendar__weekdays" aria-hidden="true">
    <span class="c-calendar__weekday">Mon</span>
    <span class="c-calendar__weekday">Tue</span>
    <span class="c-calendar__weekday">Wed</span>
    <span class="c-calendar__weekday">Thu</span>
    <span class="c-calendar__weekday">Fri</span>
    <span class="c-calendar__weekday">Sat</span>
    <span class="c-calendar__weekday">Sun</span>
  </div>

  <div class="c-calendar__grid" role="grid" aria-labelledby="due-date-month">
    <div class="c-calendar__row" role="row">
      <button
        class="c-calendar__day"
        type="button"
        role="gridcell"
        data-outside-month
      >
        <span class="c-calendar__day-label">29</span>
      </button>
      <button
        class="c-calendar__day"
        type="button"
        role="gridcell"
        aria-current="date"
      >
        <span class="c-calendar__day-label">1</span>
      </button>
      <button
        class="c-calendar__day"
        type="button"
        role="gridcell"
        aria-selected="true"
      >
        <span class="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 .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 .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="c-field c-date-picker">
  <label class="c-field__label" for="case-due-date">Due date</label>
  <div class="c-date-picker__field">
    <input
      class="c-field__control 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="c-button c-date-picker__trigger"
      type="button"
      popovertarget="case-due-calendar"
      aria-label="Choose due date"
    >
      Calendar
    </button>
  </div>
  <span class="c-field__hint" id="case-due-date-help"
    >Local SLA timezone: America/Chicago.</span
  >
  <div class="c-date-picker__popover" id="case-due-calendar" popover>
    <section class="c-calendar" aria-labelledby="case-due-month">...</section>
  </div>
</div>

Set --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 .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="c-date-range">
  <div class="c-date-range__fields">
    <label class="c-field">
      <span class="c-field__label">SLA starts</span>
      <input class="c-field__control" name="sla_start" value="2026-06-16" />
    </label>
    <label class="c-field">
      <span class="c-field__label">SLA ends</span>
      <input class="c-field__control" name="sla_end" value="2026-06-20" />
    </label>
  </div>
  <section class="c-calendar" aria-labelledby="sla-month">
    <h2 class="c-calendar__heading" id="sla-month">June 2026</h2>
    <div class="c-calendar__grid" role="grid" aria-labelledby="sla-month">
      <div class="c-calendar__row" role="row">
        <button
          class="c-calendar__day"
          type="button"
          role="gridcell"
          data-range-start
          aria-selected="true"
        >
          <span class="c-calendar__day-label">16</span>
        </button>
        <button
          class="c-calendar__day"
          type="button"
          role="gridcell"
          data-in-range
        >
          <span class="c-calendar__day-label">17</span>
        </button>
        <button
          class="c-calendar__day"
          type="button"
          role="gridcell"
          data-range-end
          aria-selected="true"
        >
          <span class="c-calendar__day-label">20</span>
        </button>
      </div>
    </div>
  </section>
</div>

Time Options

.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="c-time-picker" role="listbox" aria-label="Reminder time">
  <div class="c-time-picker__grid">
    <button class="c-time-picker__option" type="button" role="option">
      08:30
    </button>
    <button
      class="c-time-picker__option"
      type="button"
      role="option"
      aria-selected="true"
    >
      09:00
    </button>
    <button class="c-time-picker__option" type="button" role="option" disabled>
      09:30
    </button>
  </div>
</div>

Class Contract

SelectorRole
.c-calendarCalendar surface wrapper.
.c-calendar__headerMonth heading and navigation row.
.c-calendar__headingCurrent month/range title.
.c-calendar__navOptional wrapper for previous/next controls.
.c-calendar__nav-buttonMonth navigation button.
.c-calendar__weekdaysSeven-column weekday label row.
.c-calendar__weekdayWeekday label, usually aria-hidden.
.c-calendar__gridSeven-column date grid.
.c-calendar__rowOptional ARIA row wrapper for one calendar week.
.c-calendar__dayIndividual date button or gridcell.
.c-calendar__day-labelOptional day number layer for selected/range art.
.c-calendar__footerOptional shortcut/status/action row.
.c-calendar__shortcut-listOptional list of quick date commands.
.c-date-pickerField + popover picker wrapper.
.c-date-picker__fieldInput and trigger row.
.c-date-picker__controlNative field control inside the picker row.
.c-date-picker__triggerButton that opens the calendar popover.
.c-date-picker__popoverNative popover element containing the calendar.
.c-date-rangeStart/end field and calendar wrapper.
.c-date-range__fieldsResponsive two-field date range grid.
.c-time-pickerTime option surface.
.c-time-picker__gridResponsive time option grid.
.c-time-picker__optionSelectable time button.

State Contract

StateAttributeNotes
todayaria-current="date" or data-todayUse aria-current="date" when it reflects the real current date.
selected dayaria-selected="true" or data-selectedApp controls single-date selection.
disabled daydisabled, aria-disabled="true", or data-disabledPrefer native disabled for real buttons that cannot be chosen.
outside monthdata-outside-monthMuted visual state for leading/trailing dates.
range startdata-range-startUsually paired with aria-selected="true".
range enddata-range-endUsually paired with aria-selected="true".
in rangedata-in-rangeApplies the connecting range fill.
invalid fieldaria-invalid="true" on a nested field controlField hint inherits the danger tone unless explicitly toned.
selected timearia-selected="true" or data-selectedWorks on .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 tabindex or another documented focus model for the grid.
  • aria-live announcements when changing months if the update is not obvious.
  • Validation timing and messaging via aria-invalid, aria-describedby, and role="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 .c-field instead. Reach for .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

TokenDefaultPurpose
--c-calendar-cell-size2.5remMinimum day-cell block size.
--c-calendar-gap--pl-space-1Gap between grid cells.
--c-calendar-radius--pl-radius-lgDay-cell radius.
--c-calendar-range-bgaccent tintFill behind in-range days.
--c-calendar-range-hover-bgstronger accent tintHover fill for in-range days.
--c-date-picker-anchor--pl-date-picker-anchorAnchor name for a picker popover.
--c-date-picker-popover-sizemin(100vi - 2rem, 23rem)Popover inline size.
--c-date-picker-popover-max-blockmin(34rem, calc(100dvb - 2rem))Popover max block size.