Skip to content

Accessibility and keyboard

None of this existed before 1.0. The old Vue component resolved initialFocus to a // TODO and handled no keys at all, which made the calendar unusable without a mouse. It is now implemented once in @taqwim/calendar-core and is therefore identical in every framework.

Every key you press is logged below. A key shown in teal is one store.handleKeydown claimed — the adapter called preventDefault() on it. Anything else fell through to the browser.

Rabi' al-awwal 1448
Calendar for Rabi' al-awwal 1448
Tab into the grid and press a key — teal means the calendar handled it.

Only one cell is tabbable at a time — a roving tabindex — so Tab enters and leaves the grid rather than walking every day of the month. PageUp/PageDown page by month, Shift with them by year.

Key
Previous / next day
Previous / next week
Home End First / last day of the week
PageUp PageDown Previous / next month
Shift + PageUp/PageDown Previous / next year
Enter Space Select the focused day

Under dir="rtl" the horizontal keys mirror: moves to the previous day, because that is the direction the eye travels. This matters for the Arabic locale the library primarily exists for.

Moving focus past the edge of the visible month pages the calendar, so the focused date is always on screen.

Exactly one cell is in the tab order at a time, so Tab enters and leaves the grid rather than walking forty-two buttons.

Before anything has been focused, that one cell is resolved in the same order initialFocus uses: the selected date, else today, else the first selectable day of the visible month. Disabled days are skipped — a minValue in the middle of a month must not strand keyboard users outside the grid.

The adapters do not compute accessibility attributes; the store does, and they spread what it returns.

  • Root: role="application" and an aria-label naming the visible month.
  • Grid: role="grid", with aria-readonly / aria-disabled when applicable.
  • Cell: role="gridcell", carrying aria-selected.
  • Cell trigger: role="button", aria-label with the full localised date, and aria-disabled when the day is blocked. It deliberately does not carry aria-selected — that attribute is invalid on role="button", and the enclosing gridcell already has it.
  • A visually hidden role="heading" announces the visible month.

@axe-core/playwright runs against every adapter on every CI run, in English left-to-right and Arabic right-to-left. It is not advisory: a violation fails the build.

That check earned its place immediately. It found the invalid aria-selected above, and two colour-contrast failures in the default theme — the muted foreground at 2.53:1 and adjacent days at 1.67:1, the latter caused by fading them with opacity.