Skip to main content

Accessibility

What's guaranteed, and — more usefully — how it's held to.

Tested, not asserted

Two suites back these claims up, and they've each caught real bugs:

  • Keyboard and focus contracts are driven with @testing-library/user-event — Tab is actually pressed, and focus is actually asserted.
  • axe runs over every component with real ARIA surface. It's what caught a progressbar with no accessible name, and aria-haspopup sitting on a <span> that doesn't support it.

@storybook/addon-a11y runs axe in a real browser against every story, which also covers colour contrast — jsdom has no paint, so a unit test can't judge it.

Focus

Modals trap focus. Dialog and CommandPalette move focus in on open, cycle Tab and Shift+Tab within themselves, and return focus to whatever opened them on close. Without a trap, Tab walks out of the modal and into the page behind the scrim — visually unreachable, but still focusable.

Focus is visible. A 2px offset ring in --primary, on :focus-visible only — so it appears for keyboard users and not on mouse clicks.

Keyboard

Component
Dialog, CommandPaletteEscape closes; focus trapped; focus restored
DropdownMenu↑/↓ (wrapping), Home/End, Escape returns focus to the trigger
TabsWAI-ARIA pattern: single tab stop (roving tabindex), ←/→, Home/End
Tree↑/↓ through visible nodes; → expands or steps in; ← collapses or steps out
Autocomplete↑/↓, Enter, Escape; aria-activedescendant follows the highlight
List, Sidebar, Pagination, WizardReal buttons — reachable and activatable

Semantics

Interactive rows are real buttons, not divs with click handlers. List renders a true <ul>/<li> so the row itself is free to be a <button> — an explicit role="listitem" on a button would override its implicit role and hide from assistive tech that the row does anything at all.

Checkboxes, radios and switches keep a real native input in the accessibility tree — visually hidden, but focusable and announced. Every visual state is driven from :checked / :focus-visible on that input.

Motion

prefers-reduced-motion is respected. Skeletons stop shimmering, dialogs and toasts stop sliding. Spinner slows rather than stopping — a frozen spinner reads as a hang.

Your part

Some things the library can't do for you:

  • IconButton requires a label. The glyph is aria-hidden; without a label the control is unusable by screen readers.
  • Set Progress's label. It defaults to "Progress" so it's never unnamed, but that tells a screen-reader user nothing. Say what's progressing.
  • Give Spinner a label that says what's loading, not just "Loading".
  • Never encode meaning in colour alone. Use Badge's dot plus a word, so status survives colour blindness and greyscale.
  • Link Tabs to their panels with panelId. Eidos renders the tablist; the panel is yours.