Accessibility

Every component ships its own semantics. None of it is optional, and none of it depends on you remembering to add an attribute.

What the components guarantee

Interactive elements are real elements. A button is a button, a dialog is a native dialog opened with showModal(), and a link that navigates is an anchor. Focus containment, Escape handling and page inerting come from the platform rather than from a hand-rolled trap that works until someone opens a second overlay.

  • Every control is reachable and operable from the keyboard.
  • Focus is always visible. Interaction states are colour-only, so a focus ring is never the thing being restyled.
  • State is announced, not implied: aria-expanded, aria-pressed, aria-current and aria-invalid track the component’s own state.
  • Anything conveyed by colour is also conveyed some other way — a word, an icon, or a shape.
  • Motion respects prefers-reduced-motion. Where an animation carried meaning, the meaning stays and the movement goes.

What is still yours

A component cannot know what a control is for. These are the places where the kit takes a prop and cannot invent a default.

YoursWhy it cannot be automatic
Accessible namesAn icon-only button needs a verb only you know. Every such component takes a label prop and several warn in development when it is missing.
Heading orderComponents render the heading level you ask for. Only the page knows whether a card title is an h2 or an h4.
Alt textMediaGallery makes it a required field rather than an optional one, because a gallery is exactly where empty alt text accumulates.
Language and directionSet lang and dir on the document. Components use logical properties throughout, so RTL works once the document says so.

How it is checked

Claims like these rot unless something enforces them. Every component and every documentation route is rendered to static markup in a test harness, and the output is asserted against a set of rules:

  • No duplicate id on a page.
  • No aria-labelledby or aria-describedby pointing at an element that does not exist.
  • No form control without a label, and no button without a name.
  • No skipped heading levels.

Contrast is measured in a real browser against computed colours rather than against the palette, because a token is only as good as the pair it ends up in.

Ids derived from data need scoping
Any id built from a value — a row key, a field name — must be prefixed with useId(). Two instances of the same component on one page will otherwise collide, and a colliding id silently points a label at the wrong control.

Testing your own screens

The two checks that catch the most, fastest: tab through the screen without touching the mouse, and read it with the colours removed. If a state disappears in greyscale it was carried by colour alone, and if a control cannot be reached by keyboard it does not exist for a real share of your users.