# Accessibility

> Accessibility: a worked guide from route registry through to a production build.

Source: [https://askrjs.com/docs/guides/accessibility](https://askrjs.com/docs/guides/accessibility)

Status: stable. Packages: @askrjs/askr.

**Published packages are authoritative.** Examples may lag behind a published contract. When guidance differs, verify the exports and TypeScript declarations in your installed package, then file an issue.

## Example

Start with semantic elements and themed components, supply application labels and descriptions, then verify the complete task with keyboard and screen reader.

```tsx
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.

## Documentation navigation

[Previous](https://askrjs.com/docs/guides/environment-configuration/index.md) | [Next](https://askrjs.com/docs/guides/testing-deterministic-applications/index.md)
