Accessibility
Focus rings, contrast, invalid states, labels and touch targets, as the components implement them today.
Tab through these to compare the two ring weights side by side. The third field is aria-invalid.
Click in here, then press Tab to see each ring.
Built on Base UI
Interactive components wrap @base-ui/react primitives, which handle roles, keyboard support and focus management for dialogs, menus, selects, switches and tooltips. My layer is mostly styling, so most of what follows is about making states visible.
Focus
Every focusable control uses focus-visible: so rings show for keyboard users and not on click. There are two weights in use: Button and Input use a 1px ring-ring, while Textarea and Switch use a 3px ring-ring/50 with border-ring. I haven't settled on one. The site also sets outline-ring/50 on everything as a base.
Contrast
Neutral text is Radix gray-11 or gray-12, which Radix designs for text contrast. destructive and success use step 11 in light and step 8 in dark so white text clears AA; see Colors.
Invalid state
Textarea and Switch style aria-invalid with a destructive border and ring. Input doesn't yet, which is a gap. FieldError renders with role="alert" so the message is announced.
Touch
On touch I follow the site's mobile rule: subtract what only makes sense with a keyboard or pointer, and make what remains comfortable. Input is 44px tall and 16px text below md. Dropdown items get py-2.5 below md. Button has touch-manipulation to stop double-tap zoom, but deliberately no hit-area expansion, because menus like the editor's sit at gap-0.5. Switch extends its hit area with an after: pseudo-element.
Motion
Dialogs, menus and tooltips fade and zoom with animate-in and animate-out classes keyed to data-open and data-closed. There is no motion-reduce: handling in the components yet. That's unfinished, and new animation should include it.
Rules
The same rules ship in DESIGN.md and the MCP server, for the agents building with this.
- must
Show focus with a
focus-visible:ring on theringtoken (focus-visible:ring-1 focus-visible:ring-ringorfocus-visible:ring-3 focus-visible:ring-ring/50), and never remove an outline without replacing it.visible-focus
- must
Keep text and its background at WCAG AA (4.5:1 for body text) in both modes by pairing each fill with its
-foregroundtoken.aa-contrast
- must
Mark a field in error with
aria-invalidand style it witharia-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20, not with colour applied by hand.aria-invalid-for-errors
- must
Associate every form control with a
LabelorFieldLabel, and give icon-only buttons ansr-onlyoraria-labelname.label-every-control
- should
Add the site's
touch-targetutility to small isolated controls so they reach 44px on coarse pointers; in dense rows, raise the real height belowmdinstead.Overlapping expanded targets in a tight row make neighbours steal each other's taps.
touch-target-for-isolated-controls