Skip to main content

Learning Track

Filter content by your role to see the most relevant sections

Token-Level Accessibility

Build accessibility from the tokens up: contrast pairs validated per mode, motion safety as a dual CSS-and-script contract, legibility and touch-target floors in the dimension tokens, and focus as a composed, tokenized ring.

Darian Rosebrook
Staff Design Technologist, Design Systems Architect
Expertise:Design Systems, Accessibility, Tokens

Why This Matters

A constraint in a document is guidance; the same constraint in a token is a default. That single promotion is the difference between a system that asks its users to remember accessibility and one whose ordinary use produces it. This page is the inventory of that promotion across the four constraint families—contrast, motion sensitivity, legibility spacing, and focus—each with its token home and the check that keeps it true.

The pattern to watch for in every family is the same three-part shape: the floor (a number with a citation), the token (the floor as data, so consumers reference a name, not a memory), and the validator (the claim, checked mechanically). Where any part is missing, the page says so—those gaps are the track's remaining work.

Core Concepts: Four Families, One Shape

Family 1: Contrast

The floor is WCAG's ratios (AA 4.5:1 body, 3:1 UI and large text; AAA 7:1/4.5:1), held as WCAG_LEVELS in utils/accessibility/tokenValidator.ts beside validateColorPair. The tokens are the semantic pairs—foreground.primary over background.primary is a declared, mode-aware combination, not a coincidence of usage. And the ramps underneath are keyed so the common answers arrive correct: 500 at ≈4.9:1 on white clears body-text AA by construction; 400 at ≈3.2:1 brackets the UI floor.

// The pair discipline, in one example
"foreground.secondary": {
  "$value": "{color.palette.neutral.600}",         // light
  "$extensions": {
    "design.paths.dark": "{color.palette.neutral.300}"
  }
}
// light: #555 on #fff → 7.45:1 (AAA)
// dark:  #aeaeae on #000 → 9.47:1 (AAA)
// The pair is the claim; the validator walks it per mode.

The rule the tokens encode: pair roles, not colors. A foreground role with an enumerated set of legal backgrounds is checkable; a foreground role meeting arbitrary hexes is a wish.

Family 2: Motion Sensitivity

The floor is SC 2.3.3: motion triggered by interaction must be suppressible. The tokens are the motion system's—the duration scale, the composite interactions, and above all the one signal reduced motion rides: prefers-reduced-motion blocks in CSS (dozens of components ship them) plus the ReducedMotionContext for scripted motion, combining the media query with a localStorage reduce-motion override so both worlds read one source of truth. The validator here is review plus the e2e walks—this family's check is the weakest of the four, and saying so is part of the inventory.

Family 3: Legibility Spacing and Targets

The floors are dimensional: dimension.tapTargetMin 44px (SC 2.5.5), dimension.actionMinHeight 36px for controls, and the spacing scale's minimums for readable rhythm. The tokens make the floor the easy path—control sizes compose from the scale (control.size.md.height = spacing.size.08, 32px visual with hit-area extension to 44), so the legal target is a token reference away. The check is e2e bounding-box assertions at the small viewport—the mobile walk the navigation suite already performs.

Family 4: Focus

Focus is the family most systems leave untokenized—and the one where tokens pay fastest, because focus styling drifts per-component within one sprint. Here it is a composed semantic token:

// ui/designTokens/semantic/focus.tokens.json
"focus": {
  "ring": {
    "$type": "composition",
    "$value": {
      "border": {
        "width": "{interaction.focus.ringWidth}",  // 2px
        "style": "solid",
        "color": "{color.palette.red.500}"
      },
      "offset": "{interaction.focus.ringOffset}",  // 2px
      "opacity": "{interaction.focus.ringOpacity}" // 0.5
    }
  },
  "ringOffset": { "$value": "{spacing.size.02}" }
}

Read what the composition buys: one ring, defined once, inheriting the accent ramp (it re-themes with brands automatically), offsetting from the element edge by a spacing step so it survives adjacent borders, and pairing with the shape system so focused pills get pill-shaped rings. The width pairs with control.border.focusWidth—the emphasis step—so focus is the border machinery one notch louder, not a separate invention per component.

The Inventory Discipline

Four families, four states of health: contrast is encoded and validated; targets are encoded and e2e checked; motion is encoded with a soft check; focus is encoded with review as its check. The page's table is deliberately that honest—an accessibility roadmap is the list of cells not yet able to say "validated."

System Roles: Owning Each Family

Design Impact

Designers own the pair judgments (which foregrounds are legal over which backgrounds) and the focus visibility choices (offset, weight) within the floors. The tokens turn each judgment into a named default.

Engineering Impact

Engineers own the validators' upkeep and the honest-check upgrades: motion needs its automated guarantee (a reduced-motion e2e assertion is the obvious next cell to fill).

Accessibility Impact

The a11y function owns the inventory review—quarterly, the table's gaps get named, prioritized, and assigned a check. The inventory is the roadmap; the roadmap is the inventory.

Design & Code Interplay

Design-side, the families appear as the material's shape: palettes with threshold-bracketing ramps, size scales with legal floors, a focus style drawn once in the library and applied everywhere. A designer cannot easily make an inaccessible pair without noticing—the checker flags it while the color is still a decision.

Applied Example: Filling the Motion Cell

Take the inventory's weakest cell—motion's check—and fill it the system way:

  1. State the claim: every component that animates honors prefers-reduced-motion: reduce by removing travel, keeping fades.
  2. Find the enumerable surface: the components with transition or animation in their CSS—a grep, not a survey.
  3. Write the check: an e2e pass that emulates the media query, opens each animated-pattern page, and asserts computed transition durations collapse to the reduced values.
  4. Update the inventory: the motion cell reads "encoded and validated"—and the next weakest cell gets its name called.

The pattern is the point: every accessibility check this system has was added exactly this way—claim, surface, check, inventory.

Constraints & Trade-offs

  • Pair enumeration vs usage freedom: declared pairs are checkable but must be grown as surfaces multiply; free-form usage is flexible and unverifiable. The system grows pairs deliberately, per new surface family.
  • Focus composition vs customization: one ring prevents drift and removes per-product expression; the offset/opacity knobs exist for the legitimate variance, and anything beyond them is a system conversation.
  • Soft checks vs no checks: review-only families fail silently; naming them in the inventory with their weak state is the honest middle until the check exists.

Common Pitfalls & Failure Modes

1. Unpaired foregrounds

/* Bad: Legal over what, exactly? */
.muted { color: var(--semantic-color-foreground-secondary); }
/* Good: The pair list exists; new surfaces add pairs, not hope */

2. Bespoke focus rings

/* Bad: Component four's own opinion */
.card:focus { outline: 3px dashed rebeccapurple; }
/* Good: The composed ring */
.card:focus-visible { outline: var(--semantic-focus-ring); }

3. Shrinking below the floor "just for density"

Tight density rescales spacing, never targets—the floor is orthogonal to density by design.

4. One-signal drift

Scripted motion reading only the media query (or only localStorage) forks the signal; CSS and script must disagree about nothing.

Verification Checklist

Additional Resources

  • Color Foundations — the ramps and pairs underneath the contrast family (/blueprints/foundations/color)
  • Motion & Duration — the reduced motion contract in full (/blueprints/foundations/motion)
  • The sources — utils/accessibility/tokenValidator.ts, semantic/focus.tokens.json, core/dimension.tokens.json, context/ReducedMotionContext.tsx

Related Concepts

Reflection Questions