Accessibility
WCAG 2.1 AASection 508Accessibility is a target every component is built against, not a pass applied afterward: WCAG 2.1 AA / Section 508. A component isn't considered done until it has correct ARIA roles/states, full keyboard operability, a visible focus ring that survives theme overrides, and a passing automated accessibility test in every theme × light/dark combination — see the Definition of Done in the project's CLAUDE.md for the exact checklist every component is held to.
What "no !important" means for you
Component CSS is written entirely with :where()-wrapped selectors, which keeps every internal rule at specificity zero. Combined with tailwind-merge deduping on each component's class prop:
- A single Tailwind utility class you pass always wins over the internal default — no
!important, no extra-specific selector, no fighting the library. This requires one line of cascade-layer setup in your own global CSS (@layer theme, base, stance, components, utilities;before your Tailwind import) — see Theming § Overriding a component's styling for why Tailwind v4's own layering makes that line necessary, not optional polish. - You inherit the responsibility that comes with that power. If you override a focus-ring color, contrast-checked background, or
prefers-reduced-motionbehavior, the library can no longer guarantee the result meets AA — the override is now your styling decision. The components hand you a compliant default and an unobstructed way to change it; they don't (and structurally can't) validate what you change it to. - This cuts the other way too: if a component's default rendering fails an accessibility check, that's a bug in the library, not something you're expected to work around with your own CSS.
Keyboard conventions used across every component
| Interaction | Convention |
|---|---|
| Tab / Shift+Tab | Moves focus between components normally. Composite widgets (see below) expose exactly one stop in the base Tab sequence, not one per internal item. |
| Enter / Space | Activates the focused control — buttons, checkboxes, toggle items, menu items. This falls out of using real <button>/<input> elements rather than reimplementing activation semantics. |
| Escape | Closes the nearest open overlay (Dialog, Popover, Tooltip) and returns focus to whatever triggered it — never left stranded on a now-hidden element. |
| Arrow keys (+ Home/End) | Move roving focus within a composite widget once it has focus — Tabs (Left/Right, or Up/Down when vertical), Dropdown Menu items (Up/Down). Radio groups get the same behavior for free from the native <input type="radio"> grouping — no reimplementation needed there. |
| Disabled controls | Removed from tab order entirely (native disabled), not just visually dimmed with clicks silently swallowed. |
Modal surfaces (Dialog) trap Tab within themselves while open — tabbing off the last focusable element wraps to the first, and back again in reverse — so keyboard users can never tab "behind" a modal into the page underneath.
Where this library departs from "use the native element"
The library's default posture is to prefer real native elements (<button>, <input>, <select>) over reimplemented ARIA widgets — it's almost always the more robust choice. NumberField is a deliberate exception: it's a text <input> with role="spinbutton" and inputmode="decimal", not a native <input type="number">. Native number inputs don't support locale-aware grouping/decimal formatting (they render digits verbatim, e.g. 1234.5 rather than 1,234.5 or 1.234,5) and don't support aria-valuetext, so a screen reader announces the raw number rather than the formatted or unit-labeled value a sighted user sees (e.g. "$19.99" or "25%"). This matches the same choice Reka UI and Ark UI make for their NumberField primitives, for the same reason. Slider (Phase 11) has an identical native-<input type="range"> limitation and will document the same departure when it ships.
How this is verified
- Automated: every component's test suite runs its rendered output through
axe-core, once per theme and once each for light and dark mode. A component isn't merged with a failing axe check in any of those combinations. - Manual: test files also carry a keyboard-nav checklist as a doc comment (tab order, activation keys, focus-visible appearance) for the interaction paths automated testing can't fully cover — jsdom doesn't render focus rings or evaluate real layout, so those get a documented manual pass alongside the automated one.
- Screen readers: state that changes without a visible page navigation — a filtered result count, a new pagination page, a toast appearing — is announced through a shared visually-hidden
aria-liveregion, not just updated silently in the DOM. Sort changes are the deliberate exception: DataTable/TreeTable updatearia-sorton the column header rather than firing a separate live-region announcement. This follows the WAI-ARIA Authoring Practices sortable-table pattern — a screen reader announces a column header'saria-sortchange on its own because the change happens on the same element that has (or had) focus, the same wayaria-expanded/aria-pressedchanges are announced without a separate live region.