Targets

The non-negotiables. If a surface fails any of these, it ships with a fix, not a waiver.

TargetValueDetail
Contrast4.5:1 body · 3:1 large + UIWCAG 2.2 AA. Body copy ≥ 16px must hit 4.5:1; ≥ 18.66px or bold ≥ 14px and non-text UI hit 3:1.
Touch targets≥ 44 × 44 CSS pxHit area, not visual size. Use padding on the interactive element to reach the floor.
Focus ring1px solid var(--fg), 2px offsetSet globally in app.css. Visible only on :focus-visible — mouse clicks stay clean.
MotionRespects prefers-reduced-motionPress-scale and per-character stagger both collapse to instant transitions under the reduce query.
KeyboardTab · Space / Enter · EscapeEvery interactive surface reachable via Tab, activated via Space or Enter, dialogs trap and Escape closes.
Focus visualization

Tab through the three controls to see the ring. We use :focus-visible only — mouse clicks never light it up.

Contrast tokens

Computed against the cream surface. Every product text colour clears AA for its size class.

Aa Body copy at 15px sits here, like this line.
Jade-deep on cream
10.2:1 Pass · AAA

Body copy. Primary --fg over --bg.

Aa Body copy at 15px sits here, like this line.
Jade on cream
5.4:1 Pass · AA

Brand accent, links. --brand over --bg.

Aa Body copy at 15px sits here, like this line.
Muted on cream
4.6:1 Pass · AA

Secondary text. --fg-muted over --bg.

Motion

Press feedback is universal across buttons. Per-character stagger lives on the marketing-side hero only. Both honour reduce-motion via @media.

The press uses transform: scale(0.97) at 100ms. Under @media (prefers-reduced-motion: reduce) the transform is removed entirely and the button state changes without animation.

Screen reader patterns
  • Semantic HTML first. Use <button>, <nav>, <main> before reaching for ARIA. Most components need no role at all.
  • aria-label on icon-only buttons. Every standalone icon trigger gets a label (close, copy, dismiss). No exceptions.
  • aria-busy for loading. Set on the region that is fetching, not the whole page, so the rest stays navigable.
  • aria-current for nav. Sidebar links and tabs set aria-current="page" or aria-current="true" on the active item.
  • Live regions for toasts. Polite by default, assertive only for errors that block the user.