@cf/ui

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 the ring token (focus-visible:ring-1 focus-visible:ring-ring or focus-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 -foreground token.

    aa-contrast

  • must

    Mark a field in error with aria-invalid and style it with aria-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 Label or FieldLabel, and give icon-only buttons an sr-only or aria-label name.

    label-every-control

  • should

    Add the site's touch-target utility to small isolated controls so they reach 44px on coarse pointers; in dense rows, raise the real height below md instead.

    Overlapping expanded targets in a tight row make neighbours steal each other's taps.

    touch-target-for-isolated-controls