Skip to content

Foundations

Accessibility

Contrast is a property of the token layer, not of individual components, so it is enforced there: every pair below is measured live from the values currently painted, and the same checks run headlessly against both themes in npm run check. A token change that breaks a ratio fails the build.

All pairs pass

Measured in the light theme, in this browser. Switch the theme in the top bar and the numbers re-measure.

Two decisions that do most of the work

Ink is the interactive colour

The brand violet sits at 2.2:1 on white — fine as a wash, unusable as a control fill, a focus ring or accent text. So the system's interactive colour is ink (near-black on light, near-white on dark) and the brand hue is reserved for moments that are about identity rather than state. Every fill, ring and pressed state therefore starts around 18:1 instead of fighting for 3:1.

Two kinds of line

--an-border is the decorative hairline on cards, dividers and table rules, which SC 1.4.11 exempts. --an-border-control bounds real controls — inputs, selects, switch tracks — and is dark enough to clear 3:1. Reaching for the hairline on an input is the most common contrast failure in minimal systems; keeping the two names apart is what stops it happening by accident.

Check your own overrides

The colour maths is exported, so a consumer changing tokens can verify the result instead of guessing — and can wire the same assertion into their own tests.

ts
import { contrastRatio, contrastLevel, readTokenColor } from '@anomalia/ui';

// Check a brand override before shipping it
const ratio = contrastRatio('#c485fe', '#ffffff');
contrastLevel(ratio);            // 'fail' — 2.2:1

// Or measure what is actually painted right now
contrastRatio(readTokenColor('--an-text')!, readTokenColor('--an-surface')!);

Keyboard

KeyBehaviour
Tab / Shift+TabMoves between controls. Overlays trap it while open and hand it back on close.
Enter / SpaceActivates buttons, rows and chips. Space is also the ripple trigger for keyboard users.
EscapeCloses dialogs, sheets, popovers and menus — unless the dialog is a non-dismissible confirmation.
ArrowsMove within a composite: Menu items, Tabs, SegmentedControl, RadioGroup, Slider.
Home / EndFirst and last item in Menu and Tabs; min and max on Slider.
PageUp / PageDownSlider, in 10% steps.
Type-aheadMenu jumps to the next item starting with the typed character.

Focus rings are drawn on :focus-visible only, so a mouse click never leaves one behind while every keyboard user always gets one. Overlays restore focus to the trigger on close, and SkipLink is available for jumping past navigation.

OS preferences

  • prefers-reduced-motion — every transition, ripple and animation collapses, and haptics are suppressed. Motion is never the only carrier of meaning.
  • prefers-contrast: more — hairlines become ink, muted text stops being muted, and the state layers get stronger. Handled in the token layer, so components inherit it.
  • prefers-reduced-transparency — frosted chrome (app bar, sheets, glass cards) turns opaque instead of blurred.
  • forced-colors (Windows High Contrast) — the OS flattens backgrounds, which would erase pill indicators and state layers. Selected and current states gain a Highlight outline, surfaces gain a real border, and decorative ripples are hidden.

Touch and pointer

  • Every interactive element carries a hit area of at least 44px, even where the painted control is smaller (see IconButton, Checkbox, Slider).
  • Inputs render at 16px on coarse pointers, below which iOS Safari zooms the viewport on focus and never zooms back.
  • Hover-only affordances are never load-bearing: Tooltip renders nothing on touch, so anything it says must exist elsewhere too.
  • Gestures are additive. A bottom sheet can always be closed with Escape or the scrim, not only by dragging.

Known limits

  • The audit covers token pairs, not composed screens: a component placed on a surface it was never designed for can still land below 3:1.
  • Disabled controls sit at 40% opacity and are exempt from contrast requirements by WCAG, but they are genuinely hard to read — do not use disabled state to convey information.
  • variant="glass" surfaces depend on what is behind them; the audit measures the opaque fallback, which is the worst case, not every case.