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
progressbarwith no accessible name, andaria-haspopupsitting 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, CommandPalette | Escape closes; focus trapped; focus restored |
DropdownMenu | ↑/↓ (wrapping), Home/End, Escape returns focus to the trigger |
Tabs | WAI-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, Wizard | Real 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:
IconButtonrequires alabel. The glyph isaria-hidden; without a label the control is unusable by screen readers.- Set
Progress'slabel. It defaults to "Progress" so it's never unnamed, but that tells a screen-reader user nothing. Say what's progressing. - Give
Spinneralabelthat says what's loading, not just "Loading". - Never encode meaning in colour alone. Use
Badge'sdotplus a word, so status survives colour blindness and greyscale. - Link
Tabsto their panels withpanelId. Eidos renders the tablist; the panel is yours.