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.

Calendar for Jumada 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.

When an enabled, available date is visible, one cell is in the tab order, so Tab enters and leaves the grid rather than walking forty-two buttons. If every visible date is blocked, none of the day buttons is a Tab stop.

Before anything has been focused, that one cell is resolved in the same order initialFocus uses: the selected date, else today, else the first enabled, available day across the visible months. Disabled and unavailable days are skipped. A blocked first month must not hide an available date in a later visible month.

The shared state controls the accessibility information. Adapters spread the store’s attribute bags and map each day’s state onto their rendered elements.

  • 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.

The shared browser suite covers Vue, React, Svelte, Solid and Angular, including keyboard focus, selection and right-to-left interaction. Angular also passes strict template checking. See e2e/KNOWN-GAPS.md in the repository for the remaining test-suite scope.