Accessibility
WCAG 2.2 AA contrast by construction, keyboard support from React Aria.
Syntara targets WCAG 2.2 AA. Two parts of that are built in rather than checked afterwards: colour contrast, which the theme engine guarantees for every brand, and keyboard and screen-reader behaviour, which comes from React Aria.
Contrast by construction
A theme that fails WCAG 2.2 AA can’t be generated. For every brand, in light and dark, the engine checks each pair below. When one fails, it moves the colour along its ramp — or nudges it off the ramp — until it passes, and records why.
| Foreground | Checked against | Minimum |
|---|---|---|
text.default | surface.canvassurface.defaultsurface.raisedsurface.sunkensurface.selected | 4.5:1 |
text.subtle | surface.canvassurface.defaultsurface.raisedsurface.sunkensurface.selected | 4.5:1 |
text.brand | surface.canvassurface.defaultsurface.raisedsurface.sunkensurface.selected | 4.5:1 |
text.inverse | surface.inverse | 4.5:1 |
action.primary.fg | action.primary.bgaction.primary.hoveraction.primary.pressed | 4.5:1 |
action.secondary.fg | action.secondary.bgaction.secondary.hoveraction.secondary.pressed | 4.5:1 |
accent.fg | accent.bg | 4.5:1 |
accent.text | surface.defaultaccent.subtle | 4.5:1 |
feedback.success.fg | feedback.success.bgsurface.canvassurface.defaultsurface.raisedsurface.selectedsurface.sunken | 4.5:1 |
feedback.warning.fg | feedback.warning.bgsurface.canvassurface.defaultsurface.raisedsurface.selectedsurface.sunken | 4.5:1 |
feedback.danger.fg | feedback.danger.bgsurface.canvassurface.defaultsurface.raisedsurface.selectedsurface.sunken | 4.5:1 |
feedback.info.fg | feedback.info.bgsurface.canvassurface.defaultsurface.raisedsurface.selectedsurface.sunken | 4.5:1 |
feedback.success.onSolid | feedback.success.solid | 4.5:1 |
feedback.warning.onSolid | feedback.warning.solid | 4.5:1 |
feedback.danger.onSolid | feedback.danger.solid | 4.5:1 |
feedback.info.onSolid | feedback.info.solid | 4.5:1 |
focus.ring | surface.canvassurface.defaultsurface.raisedsurface.selected | 3:1 (non-text) |
border.strong | surface.canvassurface.default | 3:1 (non-text) |
Ratios are the WCAG 2.x formula computed on the final 8-bit hex values, and they are never rounded up: 4.49:1 fails. Text needs 4.5:1; focus rings and input borders need 3:1 against the surfaces they sit on (1.4.11, non-text contrast).
Fuzz testing
Three hand-picked tenants prove little, so the solver is fuzzed with random brands — random primary, sometimes a random accent, random neutral, shape, type pair and density.
| Metric | Value |
|---|---|
| Random brands (seed 2026), each in light and dark | 1,000 |
| Contrast checks (118 per brand) | 118,000 |
| Passed | 118,000 |
| Failed | 0 |
| Pass rate | 100% |
| Solver adjustments per brand (min / median / max) | 0 / 4 / 7 |
| Median generation time | 0.62 ms |
| p95 generation time | 0.95 ms |
pnpm test:themes. Timing measured on Apple M1, Node v26.8.1; it varies by machine. Source: fuzz-report.md.The closest calls the solver shipped in that run:
| Required | Lowest shipped | Pair |
|---|---|---|
| 3:1 | 3.0030:1 | light focus.ring #2e97a5 on surface.selected #daf4f8 |
| 4.5:1 | 4.5001:1 | light action.primary.fg #ffffff on action.primary.bg #6d7e29 |
The report also lists how often the solver stepped in and why. One rule in it isn’t WCAG: in dark mode, primary fills must reach 2.2:1 against the canvas so a near-black brand button doesn’t vanish. It is reported as a visibility adjustment, never as a WCAG pass.
Keyboard and screen readers
Components are built on React Aria Components (ADR-002). Focus management, roving focus in lists and grids, typeahead, focus trapping in dialogs and the ARIA patterns come from there; Syntara doesn’t re-implement them. Each component page lists its keyboard interactions, taken from its meta.json.
Built into every component
- Visible focus. A 2px ring in
--syntara-color-focus-ring, checked at 3:1 against the canvas, default and raised surfaces in every tenant. - Target size. Controls are at least 24 × 24 px (2.5.8), compact density included.
- Not by colour alone. Status pairs colour with an icon and a word.
- Reduced motion. Animations run only under
prefers-reduced-motion: no-preference. - Names. Icon-only buttons require an
aria-labelin their types; decorative icons are hidden from assistive technology. - Direction. Right-to-left layouts and keyboard behaviour mirror from the locale — see RTL.
What isn’t proven yet
Contrast is guaranteed by the engine and fuzzed. Component-level checks — axe on every component state, per tenant and scheme — are part of Phase 2 and not reported here until they run.