Contrast: WCAG minimums and APCA
Two bars apply together, and both are enforced in the registry’s token build. The WCAG 2 minimums (owner, 2 November 2026) are a floor on the ratio; APCA 3.0 — the Advanced Perceptual Contrast Algorithm — is the perceptual bar the palette is solved against. The two models disagree on some pairs, so a colour has to clear both.
Body and non-body text are measured on both
base and surface, in both themes. The APCA
bars sit beside them:
Five button labels hold 4.5:1, not 7:1
One exception is named and owner-confirmed. These five roles hold 4.5:1 on their fill instead of 7:1 (APCA Lc 75 still applies):primary-foregroundbrand-accent-foregroundapp-accent-foregroundsidebar-primary-foregrounddestructive-foreground
-aa) tier, which is 4.5:1
under white by definition. Holding 7:1 would darken every brand’s action colour, so the
exception stays until the owner rules otherwise. It covers those five roles and nothing else:
running text and a label on any other fill need 7:1.
Why two bars
Sodalite’s publisheddarkHex is #3D5AFE. Against a near-black background it passes a
WCAG ratio check and scores APCA Lc -25.8 — below even the Lc 30 floor for non-text UI.
The ratio says fine; the perceptual model says unreadable; where the two disagree, the
perceptual one is right, which is why the ratio is a floor and not the whole test.
A ratio check will also mislead you about polarity. It is not polarity-aware, so it tests a
dark-mode colour against a light background and demands a threshold on both — and satisfying
that pushes both values to mid-tone and serves neither theme.
Testing a new pairing
- Test in both light and dark.
- Check the WCAG ratio against the tier above and the APCA Lc against its row.
- Check the pairings that actually ship —
--muted-foregroundon--muted,--foregroundon--card— not only foreground on background.
mzizi_check_accessibility, which computes
the ratio locally, gives the AA, AAA and 7:1 verdicts, and does not compute APCA or simulate
colour-vision deficiency. See the MCP server.
Touch targets
Every interactive control keeps a 49px hit region, on every device, desktop included. That is the Bundu floor (owner, 2 November 2026): 7 × 7, above Apple’s 44 pt. Nothing interactive has a smaller hit region. On touch, controls are also drawn at 49px; on a desktop a button-like control may be drawn smaller (28 to 40px) and keeps the 49px region through a transparent pseudo-element. The full rule, the sizes and the migration are on the Bundu floor. The space around controls has a floor too: 14px inset, 7px gaps and 1.5 body line height, on the spacing floor.
Use the size roles rather than
h-12 or h-14:
Keyboard navigation
Interactive components use Radix UI primitives, which handle:- Focus management — trapping focus in modals, restoring it on close.
- Arrow-key navigation — within menus, tabs, radio groups and comboboxes.
- Escape to close — dialogs, dropdowns, popovers, sheets.
- Enter and Space to activate — buttons, checkboxes, toggles.
- Home and End — first and last item in a list.
Focus indicators
Every focusable element shows a visible ring, 2px with a 2px offset, using the--ring token
(cobalt):
Screen readers
Semantic HTML
Use the element that means what you intend:<button> for actions rather than a div with an
onClick, <a> for navigation, <nav> and <main> as landmarks, and heading levels in
order with none skipped.
ARIA
Radix components carry the ARIA attributes they need. For your own:LiveRegion from the harness rather than building their own — see
placing a component.
Testing
Test with at least one screen reader: VoiceOver on macOS and iOS, NVDA on Windows, TalkBack on Android.Colour independence
Never use colour as the sole carrier of meaning. Pair it with a text label (“Error: invalid email”, not a red border alone), an icon, or a pattern. The mineral palette was chosen with common colour-vision deficiencies in mind, which reduces the problem but does not remove it — two minerals that are distinguishable to you may not be to a reader.Checklist
- Contrast meets the WCAG minimum (7:1 body, 4.5:1 non-body, 3:1 UI) and the APCA threshold, in both themes
- Every control has a 49px hit region, and text is 17px body and 14px at the smallest
- Every interaction is reachable by keyboard
- Focus indicators are visible and unmodified
- A screen reader announces the component’s purpose and state
- Colour is not the only signal
- Heading levels are sequential