Accessibility
The target is WCAG 2.2 AA, asserted mechanically rather than promised.
The floor is a build step, not a policy
Every colour that declares an obligation is measured against the surface it renders on, in BOTH themes, and the build fails if it is under its floor.
$ pnpm --filter @gridd/ui check:tokens ok tokens: 53 colours, 22 contrast obligations, all above floor There is no hand-typed ratio anywhere in the system. A number typed by a person goes stale the first time either side of the pair moves, and a stale ratio is worse than no ratio because it reads as verified. The generator computes the real value, writes it into tokens.css as a comment, and refuses to emit a file that fails.
What is checked
Read from the token model, so this list is the set of assertions the build actually runs.
--gd-inkon --gd-surfaceAA--gd-ink-bodyon --gd-surfaceAA--gd-ink-secondaryon --gd-surfaceAA--gd-ink-mutedon --gd-surfaceAA--gd-ink-fainton --gd-surfaceAA-large--gd-ink-inverseon --gd-surface-inverseAA--gd-ink-on-solidon --gd-surface-solidAA--gd-ink-on-accenton --gd-accentAA--gd-ink-on-accent-bodyon --gd-accentAA--gd-ink-on-accent-mutedon --gd-accentAA-large--gd-line-controlon --gd-surfaceAA-nontext--gd-accenton --gd-surfaceAA-nontext--gd-accent-inkon --gd-surfaceAA--gd-accent-inkon --gd-accent-washAA--gd-successon --gd-surfaceAA-nontext--gd-success-inkon --gd-surfaceAA--gd-success-on-solidon --gd-successAA-nontext--gd-infoon --gd-surfaceAA-nontext--gd-info-inkon --gd-surfaceAA--gd-info-on-solidon --gd-infoAA-nontext--gd-cautionon --gd-surfaceAA-nontext--gd-caution-inkon --gd-surfaceAA--gd-caution-on-solidon --gd-cautionAA-nontext--gd-dangeron --gd-surfaceAA-nontext--gd-danger-inkon --gd-surfaceAA--gd-danger-on-solidon --gd-dangerAA-nontextAA is 4.5:1, for body text. AA-large is 3:1, for type at 18.66px or 14px bold. AA-nontext is 3:1 under WCAG 1.4.11 — control boundaries, focus rings, and any graphic you must perceive to operate the thing.
Non-negotiables
- If it is clickable, make it the element. A
divwith a click handler answers Enter, silently ignores Space, and is unreachable without a tabindex. Every interactive component here renders a realbuttonor a real link. - One focus ring, declared once. The solid accent at 2px —
base.cssdraws it withoutline, and a control that owns its own ring (input, textarea, select, the hero search pill) setsbox-shadow: var(--gd-border-focus-ring), which is the same colour and weight but follows a custom radius. An invalid control swaps only the hue (--gd-border-focus-ring-invalid); the geometry never changes. - A wash is not an indicator. The form controls used a 14%-alpha halo until 2026-08-02 — pretty, and nowhere near the 3:1 that 1.4.11 asks of a focus indicator.
--gd-accent-ringsurvives as emphasis (the selected parcel's glow), never as focus. - The ring is never removed locally. If a component needs it somewhere else — on the pill around an input rather than the input — it must PUT it there; taking an indicator away is only allowed once a better one is visible in its place.
- A colour is never the only carrier. An alert announces its tone in words to a screen reader. The approval stack is legible in pure greyscale.
aria-modalis a promise the background is inert. Either keep it or do not claim it — which is why the dialog is built on the native element rather than a div.- A placeholder is not a label.
--gd-ink-placeholderfails AA on purpose; if a placeholder is carrying information, that is a defect in the form. - Reduced motion is honoured once, globally, so a new animation is covered the moment it is written.
- Inputs floor at 16px on touch, so iOS does not zoom the page on focus.