Accessibility
A11y is a hard requirement, not a phase-2 polish. Every component asserts
axe violations === 0 at unit-test time, and behavior + ARIA + keyboard
semantics come from Radix UI primitives where
applicable.
What's enforced automatically#
- Unit-test
axechecks. Each component renders + asserts no violations viavitest-axe. CI fails on regressions. - Focus management. Radix handles focus traps + roving focus on overlays
and menus; primitive components include
focus-visible:ring-[3px] ring-accent-dimrings. - Reduced motion.
tokens.csszeroes durations underprefers-reduced-motion: reduce— components don't need to handle that themselves.
What you (the contributor) need to do#
- Use semantic HTML. Never
<div onClick>for an interactive thing. - Label icon-only controls. Pass
aria-labelto<IconButton>. - Test with the keyboard. Tab through your component. If you can't operate it without a mouse, it's not ready.
- Run
pnpm testbefore opening a PR —vitest-axeruns against every component's test file and catches the bulk of issues.
Recurring axe pitfalls (and the fixes)#
When you author a new component and the has no a11y violations test fails,
check these first — they cover the bulk of issues we've hit in this library:
-
role="grid"requiresrole="row"parents. If you don't implement true ARIA grid keyboard nav, drop the grid roles. Buttons in a CSS grid are fine without them. -
Nested-interactive on file pickers. A
role="button"div around a focusable<input type="file">violatesnested-interactive. Use a<label>wrapper — labels are spec-allowed to wrap form controls. -
Radix Dialog without a description. Radix infers
aria-describedbyfrom a sibling<DialogDescription>. If your dialog body is custom, setaria-describedby={undefined}onRadixDialog.Contentand pass anaria-labelon the content.
Color contrast#
Semantic tokens are tuned for WCAG AA in both themes. The light theme drops
accent luminance from ~0.82 to ~0.72 OKLCH and shifts accent-text to a
darker tone — not for aesthetics, for contrast. If you bypass semantic tokens
and reach for raw OKLCH, you're on the hook for re-checking contrast.