Accessibility. Measured, not assumed.
Six themes, two modes, and one rule: every ink clears WCAG AA on what it sits on. A test measures it on every change, and this page measures it live.
No single ink reads on all six brands, so each theme sets two: --fg-on-brand for text on a brand fill, and --fg-brand-text for brand-colored text. The contrast test in the registry checks every pair below in all twelve theme and mode combinations. Switch the theme to see the numbers move.
| sample | ink | on | ratio | floor |
|---|---|---|---|---|
| Aa 12px | --fg-on-brand | --fg-brand | - | 4.5: text on a brand fill |
| Aa 12px | --fg-brand-text | --bg-canvas | - | 4.5: brand text on the page |
| Aa 12px | --fg-brand-text | --bg-card | - | 4.5: brand text on a card |
| Aa 12px | --fg-brand-text | --bg-surface-brand over --bg-canvas | - | 5: brand text on the tint |
| Aa 12px | --fg-primary | --bg-canvas | - | 4.5: body text |
| Aa 12px | --fg-secondary | --bg-card | - | 4.5: secondary text on a card |
| Aa 12px | --fg-muted | --bg-canvas | - | 4.5: metadata on the page |
| Aa 12px | --fg-muted | --bg-card | - | 4.5: metadata on a card |
| Aa 12px | --fg-muted | --bg-overlay | - | 4.5: metadata in a dialog |
| Aa 12px | --status-success-fg | --status-success-soft over --bg-canvas | - | 4.5: a soft success badge |
| Aa 12px | --status-error-fg | --bg-card | - | 4.5: a field error on a card |
Links get a 2px brand outline. Buttons and menu items draw their own focus, because the reset removes outlines from them. A bare button with no style of its own takes the .focus-ring class, a box-shadow ring that survives the reset.
Hidden until the first Tab, then a mono chip with a $ in the top left. Point it at your main landmark.
The global reset stops every CSS animation and transition for people who ask for less motion. Motion components animate through JavaScript, which that reset never reaches, so every one of them calls useReducedMotion() and drops its movement, delays and springs. The .type-* classes zero their delays too, except .type-late, whose delay is the wait itself.
- Glyphs are hidden. ◆, $, // and the window dots carry aria-hidden, so a label is read as its words.
- Animated text keeps a real copy. TypeIn and RollingNumber hide their animated pieces and put the sentence or the number in an sr-only element. Not aria-label: a span has no role, so the label would be ignored.
- Forms are wired. Field points the control at its error or hint with aria-describedby and sets aria-invalid, and the error is announced as an alert.
- Current means current. Route tabs and the sidebar mark the active item with aria-current="page", the outline with aria-current="location", filter pills with aria-pressed.
ThemeScript and ModeScript set data-theme and data-mode on <html> before React loads, so the page never flashes the wrong mode. React then sees attributes it did not render. Add suppressHydrationWarning to your <html> to tell it that is expected.