Accessibility

All picker components ship with built-in accessibility support. Keyboard navigation, ARIA attributes, and focus management work out of the box in every variant — Styled, Tailwind v3/v4, and Headless.

No extra configuration is needed for standard use. This page documents what’s included so you can audit, extend, or replicate the patterns in custom UIs.

Keyboard Navigation

Calendar Pickers

KeyAction
ArrowLeftMove focus to previous day
ArrowRightMove focus to next day
ArrowUpMove focus to previous week (same weekday)
ArrowDownMove focus to next week (same weekday)
Enter / SpaceSelect the focused day
EscapeCancel changes and close the popup
TabMove to next focusable element (browser default)

Disabled date skipping: When navigating with arrow keys, disabled dates are automatically skipped. The picker searches up to 365 days in the direction of movement to find the next enabled date.

Auto-month scrolling: When keyboard focus moves to a day in a different month, the calendar view updates to show that month.

Time Picker

KeyAction
EscapeCancel changes and close the popup

Time selection uses a scroll-wheel UI with click/touch interaction. The scroll columns use scroll-snap for precise value selection.

ARIA Attributes

Every interactive element has appropriate ARIA roles and attributes:

ElementAttributeValue
Popup containerrole"dialog"
Inline containerrole"group"
Containeraria-labelPlaceholder text from locale
Containeraria-activedescendantID of the currently focused day cell

Trigger Button

AttributeValue
aria-expandedtrue when popup is open
aria-haspopup"dialog"

Calendar Grid

ElementAttributeValue
Grid wrapperrole"grid"
Week rowrole"row"
Weekday header cellrole"columnheader"
Day cellrole"gridcell"
Day cellaria-selectedtrue when selected
Day cellaria-current"date" when the cell is today
Day cellaria-labelFormatted date string (e.g. "2026-03-04")
Day celldisabledSet on disabled dates (native HTML attribute)
ElementAttributeValue
Previous month buttonaria-label"Previous month" (from locale.prevMonthLabel)
Next month buttonaria-label"Next month" (from locale.nextMonthLabel)
Month dropdownaria-label"Select month" (from locale.selectMonthLabel)
Year dropdownaria-label"Select year" (from locale.selectYearLabel)

Time Columns

ElementAttributeValue
Hour columnrole"listbox"
Hour columnaria-orientation"vertical"
Hour columnaria-label"Hours" (from locale.hourLabel)
Minute columnrole"listbox"
Minute columnaria-orientation"vertical"
Minute columnaria-label"Minutes" (from locale.minuteLabel)
Second columnrole"listbox"
Second columnaria-orientation"vertical"
Second columnaria-label"Seconds" (from locale.secondLabel)
Time itemrole"option" (padding items: "presentation")
Time itemaria-selectedtrue when the item is the current value
Period togglearia-labelCurrent period + ”, toggle AM/PM”
Clear buttonaria-label"Clear" (from locale.clear)

All label strings are customizable through the locale system.

Focus Management

Roving Tabindex

The calendar grid uses the roving tabindex pattern:

  • Only the currently focused day cell has tabIndex={0} (tabbable).
  • All other day cells have tabIndex={-1} (not tabbable, but focusable via arrow keys).
  • This means Tab moves focus out of the calendar grid (to the next control), and arrow keys move focus within the grid.

Popup mode uses FloatingFocusManager from @floating-ui/react to:

  • Trap focus within the popup while open.
  • Return focus to the trigger button when the popup closes.
  • Prevent tabbing out of the popup to background content.

Native Elements

Month and year selection use native <select> elements, which are inherently keyboard accessible. Day cells use native <button> elements with appropriate disabled states.

WCAG 2.1 Compliance

The following WCAG 2.1 success criteria are addressed:

CriterionLevelHow It’s Addressed
1.3.1 Info and RelationshipsASemantic HTML — <button>, <select>, grid/gridcell roles
2.1.1 KeyboardAAll functionality reachable via keyboard (arrow keys, Enter, Escape)
2.4.3 Focus OrderALogical tab order; popup uses focus manager
2.4.7 Focus VisibleAA:focus-visible outline (2px solid primary) on all interactive elements
4.1.2 Name, Role, ValueAAll interactive elements have accessible names via aria-label or text content
📝 Note

Styling packages (Styled, Tailwind v3/v4) include visible focus indicators by default. If you build a custom UI with the headless package, you must provide your own focus styles.

Headless: Building Accessible Custom UIs

If you’re using the headless package directly, ensure your custom UI includes:

1. Apply keyboard handler

Pass handleKeyDown to your popup container so arrow key navigation works:

const { handleKeyDown, ... } = useDatePicker({ value, onChange });
<div onKeyDown={handleKeyDown}>
{/* calendar grid */}
</div>

2. Set correct tabIndex on day cells

Use the isFocused flag from getDayProps():

const dayProps = getDayProps(date);
<button
role="gridcell"
tabIndex={dayProps.isFocused ? 0 : -1}
aria-selected={dayProps.isSelected}
aria-current={dayProps.isToday ? "date" : undefined}
aria-label={locale.formatDate(date)}
>
{dayProps.day}
</button>;

3. Add ARIA roles to your grid

<div role="grid" aria-label={locale.placeholder}>
<div role="row">
{locale.weekdays.map((wd) => (
<div key={wd} role="columnheader">
{wd}
</div>
))}
</div>
{calendar.weeks.map((week, i) => (
<div key={i} role="row">
{/* day cells with role="gridcell" */}
</div>
))}
</div>

See Building Custom UI for a complete walkthrough.