Component library
Accessible by construction
Five interaction patterns built to the WAI-ARIA Authoring Practices. Every widget below is fully operable with a keyboard, keeps its ARIA state in sync with what you see, and announces changes to assistive technology. Unplug your mouse and try them.
Modal dialog
role="dialog" · aria-modal="true"
A contained, blocking surface. While it is open the rest of the page is marked
inert, so neither Tab nor a screen reader cursor can escape it.
Tab cycles inside the dialog · Shift+Tab reverses · Esc closes · focus returns to the trigger
Tabs
role="tablist"
Only the selected tab is in the tab order — arrows move between tabs and activate them,
and panels that are not shown carry the hidden attribute rather than being
merely pushed off-screen.
What the pattern is made of
Three parts, all of them labelled: the trigger, the surface it controls, and the state that links them.
- The trigger carries
aria-expandedso the state is readable without sight. - The controlled region is referenced by
aria-controls, which must resolve. - Hover, focus and selected styles are deliberately distinct.
Keyboard behaviour
Arrow keys move and activate in one step (automatic activation). The tab row keeps a single tab stop.
- ← / → move between tabs, wrapping at both ends.
- Home and End jump to the first and last tab.
- Tab moves into the visible panel, whose content is then reachable.
Wiring, in full
Every id below resolves to a real element on this page — that is asserted in the self-test.
<div role="tablist" aria-label="…"> <button role="tab" id="tab-anatomy" aria-selected="true" aria-controls="panel-anatomy" tabindex="0">Anatomy</button> </div> <div role="tabpanel" id="panel-anatomy" aria-labelledby="tab-anatomy" tabindex="0">…</div>
←→ switch tabs · Home/End first / last · Tab enters the open panel
Combobox
aria-activedescendant
An editable text field with a filtered listbox. The input keeps and owns DOM focus;
the highlighted option is communicated with aria-activedescendant, so the
listbox itself is never focusable.
Type to filter · ↓↑ move the highlight · Enter accepts · Esc reverts and closes
Accordion
disclosure group
Independent disclosures — several may be open at once. Regions animate on height, and
hidden is only applied once the closing animation has finished, so
assistive technology never sees a half-collapsed region.
Copy the primitives you need. There is no runtime dependency: each component is a single behavioural module plus its stylesheet layer.
Nothing is fetched at runtime, so the library works in air-gapped builds, offline docs and print stylesheets alike.
The one rule: focus is never destroyed. Closing a surface returns focus to whatever opened it, and every surface that traps focus offers a documented way out.
Focus indicators here are a 3px ring with a 2px offset — visible against both the tinted stage background and white cards.
Every animation is expressed as a duration token, and the whole page collapses to
near-zero durations under prefers-reduced-motion: reduce.
Content stays visible in that mode — the region is opened and closed instantly instead of sliding.
Tab to a header · Enter / Space toggles · multiple panels may stay open
Keyboard contract
The short version, if you only remember one line per component:
- Dialog — Tab is trapped · Esc closes · focus returns to the trigger.
- Menu — ↓ opens and focuses the first item · ↑↓ wrap · Esc closes and refocuses the button.
- Tabs — one tab stop · ←→ activate · hidden panels are
hidden. - Combobox — typing filters · ↑↓ moves
aria-activedescendant· Esc reverts. - Accordion — Enter/Space toggles · height animates ·
hiddenlands at the end.