Accessibility

[v0.2]

Accessibility is part of the system, not a pass at the end of it. A colour is only a token here once its pairings clear WCAG 2.2 AA in both registers; a component is only done once it can be read, named and reached by keyboard.

Nothing on this page is a claim typed by hand. The ratios are measured from the CSS that ships, by the same functions that fail the build — so the page cannot say pass while the build says fail.

How it is enforced

Gate

npm run a11y

Before every build — locally and on Vercel. A failure stops the deploy.

Measures every sanctioned colour pairing in both registers, every text-colour utility in the source, faded text, removed focus rings, saturated accents set as text, the generated-asset palettes and the group marks. Checks the TypeScript token copy against the CSS.

Audit

npm run a11y:audit

On every pull request, in CI, against the production build.

Opens every page in Chromium in both registers and runs axe-core (WCAG 2.0–2.2 A/AA and best practice): rendered contrast, names and labels, landmarks, headings, target size. Then emulates reduced motion and fails any page where CSS still animates.

Review

the checklist below

When a new component, diagram or generator is added.

What a machine cannot judge: whether colour is the only carrier, whether canvas motion can be paused, whether a posted asset has alt text.

The rules

  1. 01

    Text contrast

    WCAG 1.4.3

    Text clears 4.5:1 against what it sits on, in both registers; 3:1 only from 24px regular or 18.66px bold. Saturated accents — dusk, ocean, teal, the domain colours — are never text: their -ink forms are.

    • [gate]Every sanctioned pairing is measured in both registers from the shipped CSS.
    • [gate]Every text-xco-* utility in the source is measured on every text surface, including opacity modifiers and placeholder text.
    • [gate]Inline colour styles may not use a saturated domain colour.
    • [audit]axe color-contrast on every rendered page, both registers.
  2. 02

    Non-text contrast

    WCAG 1.4.11

    Marks, input borders, focus rings and any graphic needed to understand the page clear 3:1 against their ground.

    • [gate]Domain and meaning colours on paper, field borders and the focus ring are sanctioned pairings.
    • [gate]Every group mark's glyph clears 3:1 on its ground.
  3. 03

    Never colour alone

    WCAG 1.4.1

    Colour reinforces; something else carries. Semantic meanings pair every colour with a shape, group marks pair every ground with an aperture angle, links are underlined or otherwise distinct from body text.

    • [gate]The group-mark pair audit fails any two marks weak on both colour and angle.
    • [audit]axe link-in-text-block.
    • [review]A new diagram or status reads correctly in greyscale.
  4. 04

    Focus is always visible

    WCAG 2.4.7, 2.4.11

    Every focusable element shows the system focus ring — 2px of full ink, offset 2px — when reached by keyboard. Components may restyle focus; they may not remove it.

    • [gate]outline-none fails unless the same class list gives a focus-visible outline or ring.
    • [gate]The focus ring token is a sanctioned 3:1 pairing on every surface.
  5. 05

    Everything has a name

    WCAG 1.1.1, 4.1.2, 1.3.1

    Images have alt text; decorative ones have empty alt or aria-hidden. Every control has an accessible name — a visible label first, aria-label only for icon-only controls. SVG and canvas that carry meaning take role="img" and a label.

    • [audit]axe image-alt, svg-img-alt, button-name, label, select-name, link-name, aria-*.
  6. 06

    Structure

    WCAG 1.3.1, 2.4.6, 3.1.1

    One h1 per page, headings in order, a main landmark, the page language set. The document outline reads as the page does.

    • [audit]axe page-has-heading-one, heading-order, landmark-one-main, html-has-lang, region.
  7. 07

    Targets

    WCAG 2.5.8

    Pointer targets are at least 24×24px, or spaced as if they were. Primary controls — buttons, toggles, the nav — are 44px high.

    • [audit]axe target-size (24px).
    • [review]Primary controls at 44px.
  8. 08

    Motion

    WCAG 2.3.3, 2.2.2

    Motion respects prefers-reduced-motion. Anything that moves for more than five seconds can be paused. Animation never carries information that a still frame does not.

    • [audit]With reduced motion emulated, no CSS animation or transition runs on any page.
    • [review]Canvas animation (the event-series cards) starts paused under reduced motion and has a pause control.
  9. 09

    Exported assets

    WCAG 1.4.3, 1.1.1

    Text inside generated images meets the same contrast bars as the page, since it is read on phones at a fraction of its size. When an asset is posted, its text goes in the alt text or the post itself.

    • [gate]Every event-series palette's text pairings are measured.
    • [review]Alt text written when posting to Luma, LinkedIn or Instagram.

Text colours

The six colours text may be set in, on the three surfaces text may sit on. Every cell clears 4.5:1. Structural paper is for rules and borders, never text; saturated dusk, ocean, teal and the domain colours are fills and strokes — their -ink forms are the text.

Paper register
Textpaperraisedquiet
xco-inkBody text, headings, UI
Aa✓ 14.45:1
Aa✓ 16.29:1
Aa✓ 12.98:1
xco-ink-mutedSecondary text, labels, hints
Aa✓ 7.23:1
Aa✓ 8.15:1
Aa✓ 6.50:1
xco-ink-weakCaptions and annotations
Aa✓ 5.08:1
Aa✓ 5.72:1
Aa✓ 4.56:1
xco-dusk-inkAccent text — the ink-safe dusk
Aa✓ 5.12:1
Aa✓ 5.77:1
Aa✓ 4.60:1
xco-ocean-inkStructural accent text
Aa✓ 7.18:1
Aa✓ 8.09:1
Aa✓ 6.45:1
xco-teal-inkOpen-register accent text
Aa✓ 5.12:1
Aa✓ 5.77:1
Aa✓ 4.60:1
Ink register
Textpaperraisedquiet
xco-inkBody text, headings, UI
Aa✓ 14.45:1
Aa✓ 12.37:1
Aa✓ 15.44:1
xco-ink-mutedSecondary text, labels, hints
Aa✓ 9.38:1
Aa✓ 8.03:1
Aa✓ 10.02:1
xco-ink-weakCaptions and annotations
Aa✓ 5.89:1
Aa✓ 5.04:1
Aa✓ 6.29:1
xco-dusk-inkAccent text — the ink-safe dusk
Aa✓ 5.38:1
Aa✓ 4.60:1
Aa✓ 5.75:1
xco-ocean-inkStructural accent text
Aa✓ 5.38:1
Aa✓ 4.60:1
Aa✓ 5.75:1
xco-teal-inkOpen-register accent text
Aa✓ 5.40:1
Aa✓ 4.62:1
Aa✓ 5.77:1

Every pairing

The registry in lib/a11y/pairings.ts. Adding a colour to the system means adding its pairings there. A pairing that cannot pass is not an exception — it is not a pairing.

Every sanctioned colour pairing, measured in both registers
PairingWhereBarPaperInk
xco-ink/xco-paperBody text, headings, UItext · 4.5:1✓ 14.45:1✓ 14.45:1
xco-ink/xco-paper-raisedBody text, headings, UItext · 4.5:1✓ 16.29:1✓ 12.37:1
xco-ink/xco-paper-quietBody text, headings, UItext · 4.5:1✓ 12.98:1✓ 15.44:1
xco-ink-muted/xco-paperSecondary text, labels, hintstext · 4.5:1✓ 7.23:1✓ 9.38:1
xco-ink-muted/xco-paper-raisedSecondary text, labels, hintstext · 4.5:1✓ 8.15:1✓ 8.03:1
xco-ink-muted/xco-paper-quietSecondary text, labels, hintstext · 4.5:1✓ 6.50:1✓ 10.02:1
xco-ink-weak/xco-paperCaptions and annotationstext · 4.5:1✓ 5.08:1✓ 5.89:1
xco-ink-weak/xco-paper-raisedCaptions and annotationstext · 4.5:1✓ 5.72:1✓ 5.04:1
xco-ink-weak/xco-paper-quietCaptions and annotationstext · 4.5:1✓ 4.56:1✓ 6.29:1
xco-dusk-ink/xco-paperAccent text — the ink-safe dusktext · 4.5:1✓ 5.12:1✓ 5.38:1
xco-dusk-ink/xco-paper-raisedAccent text — the ink-safe dusktext · 4.5:1✓ 5.77:1✓ 4.60:1
xco-dusk-ink/xco-paper-quietAccent text — the ink-safe dusktext · 4.5:1✓ 4.60:1✓ 5.75:1
xco-ocean-ink/xco-paperStructural accent texttext · 4.5:1✓ 7.18:1✓ 5.38:1
xco-ocean-ink/xco-paper-raisedStructural accent texttext · 4.5:1✓ 8.09:1✓ 4.60:1
xco-ocean-ink/xco-paper-quietStructural accent texttext · 4.5:1✓ 6.45:1✓ 5.75:1
xco-teal-ink/xco-paperOpen-register accent texttext · 4.5:1✓ 5.12:1✓ 5.40:1
xco-teal-ink/xco-paper-raisedOpen-register accent texttext · 4.5:1✓ 5.77:1✓ 4.62:1
xco-teal-ink/xco-paper-quietOpen-register accent texttext · 4.5:1✓ 4.60:1✓ 5.77:1
paper/inkInverse text — buttons, badges, the active register toggletext · 4.5:1✓ 14.45:1✓ 14.45:1
on-accent/oceanSelected chips and toggles on oceantext · 4.5:1✓ 7.18:1✓ 7.18:1
on-accent/navyText on the blueprint groundtext · 4.5:1✓ 15.71:1✓ 15.71:1
ink/duskText on a dusk ground — never papertext · 4.5:1✓ 5.21:1✓ 5.21:1
wip-badgeThe [v0.2] / [wip] badgetext · 4.5:1✓ 4.59:1✓ 4.78:1
bio-ink/paperKicker, stat and callout labels in the bio domaintext · 4.5:1✓ 5.00:1✓ 5.40:1
bio-ink/raisedbio labels on cardstext · 4.5:1✓ 5.63:1✓ 4.62:1
bio-ink/tintThe label of a bio callout, on its tinttext · 4.5:1✓ 4.61:1✓ 4.57:1
ink/bio-paleText on a bio groundtext · 4.5:1✓ 11.36:1✓ 11.51:1
bio/paperbio marks, rules and callout barsnon-text · 3:1✓ 3.05:1✓ 4.73:1
inst-ink/paperKicker, stat and callout labels in the inst domaintext · 4.5:1✓ 5.04:1✓ 5.63:1
inst-ink/raisedinst labels on cardstext · 4.5:1✓ 5.68:1✓ 4.81:1
inst-ink/tintThe label of a inst callout, on its tinttext · 4.5:1✓ 4.64:1✓ 4.76:1
ink/inst-paleText on a inst groundtext · 4.5:1✓ 11.67:1✓ 10.92:1
inst/paperinst marks, rules and callout barsnon-text · 3:1✓ 3.05:1✓ 4.73:1
tech-ink/paperKicker, stat and callout labels in the tech domaintext · 4.5:1✓ 5.03:1✓ 5.42:1
tech-ink/raisedtech labels on cardstext · 4.5:1✓ 5.67:1✓ 4.64:1
tech-ink/tintThe label of a tech callout, on its tinttext · 4.5:1✓ 4.61:1✓ 4.67:1
ink/tech-paleText on a tech groundtext · 4.5:1✓ 10.79:1✓ 11.49:1
tech/papertech marks, rules and callout barsnon-text · 3:1✓ 3.75:1✓ 3.85:1
culture-ink/paperKicker, stat and callout labels in the culture domaintext · 4.5:1✓ 4.99:1✓ 5.68:1
culture-ink/raisedculture labels on cardstext · 4.5:1✓ 5.62:1✓ 4.86:1
culture-ink/tintThe label of a culture callout, on its tinttext · 4.5:1✓ 4.60:1✓ 4.80:1
ink/culture-paleText on a culture groundtext · 4.5:1✓ 11.36:1✓ 11.65:1
culture/paperculture marks, rules and callout barsnon-text · 3:1✓ 3.05:1✓ 4.73:1
meaning-continuity/paperThe continuity marker in diagrams — paired with its shapenon-text · 3:1✓ 4.55:1—
meaning-system/paperThe system marker in diagrams — paired with its shapenon-text · 3:1✓ 5.07:1—
meaning-risk/paperThe risk marker in diagrams — paired with its shapenon-text · 3:1✓ 4.50:1—
meaning-agency/paperThe agency marker in diagrams — paired with its shapenon-text · 3:1✓ 4.53:1—
meaning-contested/paperThe contested marker in diagrams — paired with its shapenon-text · 3:1✓ 9.33:1—
meaning-critical/paperThe critical marker in diagrams — paired with its shapenon-text · 3:1✓ 10.66:1—
field-borderInput and select borders (1.4.11)non-text · 3:1✓ 14.45:1✓ 14.45:1
focus-ringThe focus outline on every surface (2.4.7, 2.4.11)non-text · 3:1✓ 14.45:1✓ 14.45:1
focus-ring/raisedFocus on cardsnon-text · 3:1✓ 16.29:1✓ 12.37:1
shadcn-foregroundshadcn bodytext · 4.5:1✓ 14.45:1✓ 14.45:1
shadcn-cardshadcn cardtext · 4.5:1✓ 16.29:1✓ 12.37:1
shadcn-popovershadcn popovertext · 4.5:1✓ 16.29:1✓ 12.37:1
shadcn-primaryshadcn primary buttontext · 4.5:1✓ 14.45:1✓ 14.45:1
shadcn-secondaryshadcn secondary buttontext · 4.5:1✓ 14.45:1✓ 14.45:1
shadcn-mutedshadcn muted texttext · 4.5:1✓ 7.23:1✓ 9.38:1
shadcn-accentshadcn accenttext · 4.5:1✓ 5.21:1✓ 5.21:1

Not held to a ratio

The xCO logotype, including the dusk x
WCAG 1.4.3 exempts text that is part of a logo or brand name.
--rule and the border tokens
Decorative dividers. Nothing is lost if they are not seen; controls carry their own ink border.
Grain
Texture, not information.
Disabled controls
WCAG 1.4.3 exempts inactive components. They still read as disabled by shape, not colour alone.

Before it ships

  • Never colour alone — A new diagram or status reads correctly in greyscale.
  • Targets — Primary controls at 44px.
  • Motion — Canvas animation (the event-series cards) starts paused under reduced motion and has a pause control.
  • Exported assets — Alt text written when posting to Luma, LinkedIn or Instagram.