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.
Try it
Section titled “Try it”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.
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.
Keyboard
Section titled “Keyboard”| 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.
The roving tabindex
Section titled “The roving tabindex”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.
What the store emits
Section titled “What the store emits”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 anaria-labelnaming the visible month. - Grid:
role="grid", witharia-readonly/aria-disabledwhen applicable. - Cell:
role="gridcell", carryingaria-selected. - Cell trigger:
role="button",aria-labelwith the full localised date, andaria-disabledwhen the day is blocked. It deliberately does not carryaria-selected— that attribute is invalid onrole="button", and the enclosing gridcell already has it. - A visually hidden
role="heading"announces the visible month.
What is checked
Section titled “What is checked”@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.