Accessibility
Accessibility: a worked guide from route registry through to a production build.
Example
Start with semantic elements and themed components, supply application labels and descriptions, then verify the complete task with keyboard and screen reader.
import { Button, Dialog, DialogContent, DialogDescription, DialogTitle, DialogTrigger } from '@askrjs/themes/components';
<Dialog>
<DialogTrigger asChild><Button>Delete project</Button></DialogTrigger>
<DialogContent>
<DialogTitle>Delete project?</DialogTitle>
<DialogDescription>This action cannot be undone.</DialogDescription>
<DeleteProjectActions />
</DialogContent>
</Dialog>Goal and architecture
`@askrjs/ui` bakes accessibility into each component as a documented, typed contract rather than leaving it to the consumer — components like `Button`, `Toast`, `Dropdown`, and `FocusScope` each export an `_A11Y_CONTRACT` constant (e.g. `BUTTON_A11Y_CONTRACT`) describing their ARIA role, keyboard activation keys, disabled-state attributes, and focus rules, following the WAI-ARIA APG patterns. Interaction behavior is centralized in shared foundations for most of the set (the `pressable` foundation drives keyboard activation for `Button`, for instance), which is what keeps behavior consistent across those components — but it's not a blanket guarantee every interactive component funnels through the same foundation, so verify the specific component you're composing with rather than assuming.
Implementation
Use the components as designed rather than reaching for `asChild` unless you need to render a different host element — `asChild` mode preserves the child's role but switches disabled handling from the native `disabled` attribute to `aria-disabled` plus `tabindex="-1"`, since a non-native element has no built-in disabled state. `Dialog` and `Popover` build on `FocusScope` to trap and restore focus around overlays; `Menu` doesn't — it manages roving focus through its own mechanism rather than `FocusScope`. Let the component you're using own its own focus behavior rather than manually managing `tabindex` and `document.activeElement` yourself, but don't assume every overlay-style component shares one implementation underneath. For custom interactive elements outside the component set, check the relevant `*_A11Y_CONTRACT` for the closest existing component as a reference for what role, keyboard support, and ARIA attributes are expected.
Failure states
A component that does not respond to its documented keyboard shortcuts (Space/Enter for `Button`, arrow keys for `Menu`/`RadioGroup`) is a contract violation, not a style choice — check the component's `.a11y.ts` source file (compiled to `.a11y.d.ts` in the published package, but the source you'd actually read is `.a11y.ts`) for the exact keys it commits to before assuming custom behavior is fine. Disabled elements must be genuinely out of the tab order (native `disabled` or `tabindex="-1"`) — most contracts declare that explicitly through a `FOCUS_RULES` field, though not every one does, so check the specific contract rather than assuming the field is always there. A visually-disabled button a screen reader user can still tab into and activate is a real bug regardless. Overlay components that don't restore focus to their trigger on close break keyboard navigation for the next action — treat that as equivalent to a broken click handler.
Verification
Assert against the exported `*_A11Y_CONTRACT` constants directly in tests — e.g. confirm a `Button`'s rendered `role` and keyboard handling match `BUTTON_A11Y_CONTRACT.ROLE` and `.KEYBOARD_ACTIVATION` — so a regression in the component's markup fails a test instead of only showing up in manual screen-reader testing. Run keyboard-only and screen-reader passes on any custom composition of overlay primitives (`Dialog` + `FocusScope`, `Popover` + `DismissableLayer`), since contracts describe the primitives correctly but your composition can still break focus order or announcement timing.