Skip to main content
Design Systems·12 min read·

Building an Accessible Color System for Design Tokens

Learn how to architect semantic color tokens that enforce WCAG compliance by design. Covers three-tier token architecture, dark mode auditing, and CI-enforced contrast testing.

Why Accessibility Must Be Built Into Your Token Architecture

Accessibility baked in at the token layer is the only scalable approach — auditing at the component level means checking hundreds of combinations instead of dozens of token pairs. A typical design system has 20–40 semantic color tokens but potentially thousands of component-level color applications. If you defer accessibility to the component layer, you are committing to an audit effort that grows with every new component and every new state variation.

The token architecture solves this through a contract: any component that uses --color-text on --color-background is automatically compliant, because the accessibility guarantee lives in the token definition, not in the component. When you change a brand color, you update the token and re-run the token-level audit — you do not audit every button, every input, every badge.

Semantic tokens enforce the contract at definition time. Developers cannot accidentally use a text color that fails contrast, because the only available text token has been audited and verified. The accessibility review becomes part of the design token update workflow, not a separate recurring burden on every design or engineering sprint.


The Three-Tier Token Architecture

A well-structured accessible token system follows three tiers, each serving a distinct purpose:

TierLayer NamePurposeExample
1PrimitiveRaw values — the complete palette--color-blue-600: #2563eb
2SemanticRole-based aliases that define meaning--color-action: var(--color-blue-600)
3ComponentUsage-specific tokens for a specific component--button-primary-bg: var(--color-action)

Where to Perform Your Accessibility Audit

Only audit at Tier 2 (semantic tokens). This is the critical insight of the three-tier system:

  • Tier 1 (primitive tokens) are just a palette. There is no concept of foreground vs. background at this level. Auditing --color-blue-600 against --color-blue-400 is meaningless without knowing how they will be used.
  • Tier 2 (semantic tokens) define the relationship between colors — text on background, icon on surface, border against field. These relationships are the actual contrast pairs that WCAG measures.
  • Tier 3 (component tokens) inherit their accessibility guarantee from Tier 2. If --button-primary-bg resolves to --color-action and --button-primary-label resolves to --color-text-inverse, the contrast of that button is determined by the semantic token pair, not the component token pair.

Auditing at Tier 2 gives you complete coverage with a small, manageable set of pairs. A typical system has 10–20 semantic pairs that cover every surface, text role, and state.


Defining Your Semantic Color Pairs

Every semantic token should be defined as one half of a pair: a foreground token and a background token that will appear together. Document these pairs explicitly in your design system — the pairing is the accessibility contract.

Semantic PairWCAG RequirementTarget RatioUse Case
text / backgroundAA 4.5:1≥7:1 for AAABody copy, labels, paragraph text
text-secondary / backgroundAA 4.5:1≥4.5:1Supporting copy, captions, helper text
text-muted / backgroundAA 4.5:1≥4.5:1Placeholder text, metadata
text-inverse / background-inverseAA 4.5:1≥7:1 recommendedText on dark surfaces
action / action-surfaceAA UI 3:1≥4.5:1 recommendedPrimary buttons, links, CTAs
focus-ring / adjacent-surfaceAA 3:1≥3:1Focus indicators on interactive elements
error / surfaceAA 3:1 (icons), 4.5:1 (text)≥4.5:1Error states, validation feedback
success / surfaceAA 3:1 (icons), 4.5:1 (text)≥4.5:1Success states, confirmation indicators
border / surfaceAA 3:1≥3:1Input borders, dividers, card outlines
icon / surfaceAA 3:1≥3:1Informational icons, UI controls

Why text-secondary Should Still Pass 4.5:1

A common design system pattern is to define a "muted" or "secondary" text color that fails AA contrast, justified as "it is less important content." This is a WCAG violation. WCAG does not exempt text based on its perceived importance to the designer. Any text that is rendered and visible must meet the minimum contrast for its size. If secondary text is used at 14px regular weight, it must achieve 4.5:1 regardless of whether it is "primary" or "secondary" in your hierarchy.


Step-by-Step: Auditing Your Token Pairs

Follow this process whenever you introduce or modify semantic tokens:

  1. List all semantic tokens that appear as text on a background. Identify every foreground/background pair: text on default background, text on card, text on dark surface, action color on white, focus ring on gray, error on white, and so on. Document them in a spreadsheet or a dedicated contrast matrix file.
  2. Measure contrast for each pair in light mode. Use the WCAG relative luminance formula or a verified tool (Colorable, Contrast Ratio by Lea Verou, your design system's built-in checker). Record the ratio and the WCAG level it achieves (Fail, AA, AAA).
  3. Repeat the full measurement process for dark mode. Dark mode is not light mode with inverted values. Re-measure every pair independently. A pair that achieves 7:1 in light mode may achieve only 3.5:1 after naive inversion.
  4. Record results in a contrast matrix. A contrast matrix is a grid with foreground tokens as rows and background tokens as columns, with ratios in each cell. This makes it immediately visible which combinations are safe and which are not.
  5. Flag failures and propose adjusted values. For any pair that fails its required threshold, identify a candidate replacement. Typically this means lightening a text color, darkening a background, or replacing an accent color with a higher-contrast variant from the primitive scale.
  6. Lock passing pairs with a CI-enforced test. Write a build step that reads your token definitions, computes contrast ratios for all documented pairs, and fails the build if any pair drops below its required threshold. This prevents future token changes from silently introducing failures.

Dark Mode Accessibility: The Most Common Failures

Simply inverting colors does not guarantee accessibility — dark mode requires its own contrast audit.

This is the single most important dark mode design system rule. Designers frequently create dark mode by flipping light tokens to dark equivalents, then assuming that because light mode passed, dark mode also passes. This assumption is wrong often enough to cause systematic failures.

Common FailureWhy It HappensFix
Muted text on dark background fails 4.5:1Lightened text not light enough — e.g., #6B7280 on #1E1E1E achieves only ~3.4:1Use text colors with luminance high enough for the ratio: aim for ≥70% lightness on very dark backgrounds
Low-saturation accent colorsSaturated colors often lose perceived contrast when their hue shifts at dark luminance valuesIncrease lightness significantly or switch to a higher-chroma primitive step
Border-only focus indicators invisibleThin outlines have low luminance difference against dark backgrounds with similar tonesUse outline with a light, forced-colors-aware color; add a dark inner ring for contrast on both light and dark
Interactive element states not auditedDesigners audit the default state but not hover, active, or disabled states in dark modeAudit every state: default, hover, active, focus, disabled
Text on colored dark surfacesA dark teal surface with medium-lightness text can fail even if teal-on-white passesSurface tokens and text tokens must be audited as a pair in every theme variant

Practical Dark Mode Token Strategy

The most reliable approach to dark mode token accessibility is to define a separate semantic token resolution for dark mode rather than computing dark mode values algorithmically:

:root {
  --color-text: #1e293b;
  --color-background: #ffffff;
  --color-text-secondary: #475569;
}

.dark {
  --color-text: #f1f5f9;
  --color-background: #0f172a;
  --color-text-secondary: #94a3b8;
}

In this example, --color-text-secondary in light mode (#475569 on #FFFFFF) achieves approximately 7.4:1. In dark mode (#94A3B8 on #0F172A) achieves approximately 5.9:1 — both passing AA. These values were chosen through direct measurement, not formula inversion.


Building a Color Scale That Stays Accessible

A primitive color scale underpins your entire token system. If the scale is built well, finding accessible pairs is straightforward — you pick non-adjacent steps. If the scale is built poorly, you will fight contrast failures every time you try to use it.

  1. Use HSL or OKLCH instead of hex for defining your scale. Defining a scale in hex is opaque — it is impossible to see at a glance how the luminance is distributed across steps. OKLCH (the perceptually uniform color space supported in CSS Color Level 4) allows you to define each step at a consistent lightness increment, making the contrast relationships predictable.
  2. Define your scale in steps of perceived contrast, not equal hue steps. An equal-lightness increment in HSL does not produce equal perceptual contrast increments. Use a tool like Huetone or OKLCH-based scale builders that calibrate steps by perceptual lightness.
  3. Maintain a "safe zone" lookup table. For each background lightness level in your system, document which scale steps pass AA and which pass AAA. This becomes a reference designers use when choosing foreground colors without running manual calculations every time.
  4. Never use adjacent scale steps for text/background pairings. Skip at least 3 steps between a text color and its background. Adjacent steps (e.g., Blue-400 text on Blue-300 background) produce ratios well below 4.5:1. A useful rule of thumb is to skip 4–5 steps for normal text and 2–3 steps for UI component borders.
  5. Document the specific pairs that are tested and approved. In your design system documentation, publish a table of approved token pairs with their measured ratios. Designers reference this table rather than re-checking individual pairs every time.

Integrating Accessibility into the Design Token Pipeline

Design Phase: Tools for Scale Building

  • Huetone: A web-based color scale tool that shows WCAG contrast ratios for every step combination as you design your scale. Building your scale in Huetone means you see the contrast matrix in real time as you adjust hue, chroma, and lightness.
  • OKLCH Color Picker (oklch.com): Allows building token values in the perceptually uniform OKLCH space, then exporting to HEX or CSS for use in production.
  • Colorbox (by Lyft): An open-source scale builder that generates color ramps calibrated for accessibility, with WCAG ratio visualization.

Engineering Phase: CI Enforcement

Write a contrast check script that runs in your CI pipeline on every token change:

// contrast-check.js (simplified)
const tokens = require('./tokens/semantic.json');
const { contrast } = require('chroma-js');

const pairs = [
  { fg: tokens.colorText, bg: tokens.colorBackground, min: 4.5, label: 'text/background' },
  { fg: tokens.colorTextSecondary, bg: tokens.colorBackground, min: 4.5, label: 'text-secondary/background' },
  { fg: tokens.colorAction, bg: tokens.colorBackground, min: 3.0, label: 'action/background' },
];

pairs.forEach(({ fg, bg, min, label }) => {
  const ratio = contrast(fg, bg);
  if (ratio < min) {
    console.error(`FAIL: ${label} — ${ratio.toFixed(2)}:1 (required ${min}:1)`);
    process.exit(1);
  }
});

Component Phase: Storybook Integration

The @storybook/addon-a11y package runs axe-core on every story. When your token pairs are correctly established at the semantic level, this addon should report zero contrast failures across all stories. Any failure in Storybook indicates either a component-level override that bypasses the token system or a token audit gap.


Exporting Accessible Tokens

A token system is only useful if it reaches every platform and tool your team uses. Here are the primary export targets and their formats:

FormatSyntaxNotes
CSS Custom Properties--color-text: #1e293b;Standard approach; :root {} for light, .dark {} for dark mode override
Tailwind CSSextend.colors in tailwind.config.jsUse semantic names, not scale names — text-primary not slate-800
W3C Design Tokens Format{ "$value": "#1e293b", "$type": "color" }Emerging community standard (DTCG), growing tool support
Style DictionaryJSON source → platform transformsSingle source file generates CSS variables, iOS Swift, Android XML, and more
Figma VariablesPlugin JSON via Tokens StudioSyncs design tokens with Figma's native Variables feature for designer-developer handoff

Style Dictionary: Single Source of Truth

Style Dictionary (by Amazon) is the most mature solution for multi-platform token export. You maintain one JSON source file and configure platform-specific transforms:

{
  "color": {
    "text": {
      "default": {
        "$value": "#1e293b",
        "$type": "color",
        "$description": "Primary text on default background. Ratio: 16.0:1"
      }
    }
  }
}

The $description field is where you embed the audited contrast ratio directly in the token definition. Any developer looking at the token sees immediately that this value has been verified and at what ratio.

Tailwind Configuration

When exporting to Tailwind, maintain the semantic layer in your config rather than referencing primitive scale values directly:

// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      colors: {
        text: {
          DEFAULT: 'var(--color-text)',
          secondary: 'var(--color-text-secondary)',
          inverse: 'var(--color-text-inverse)',
        },
        background: {
          DEFAULT: 'var(--color-background)',
          inverse: 'var(--color-background-inverse)',
        },
      },
    },
  },
};

This approach lets Tailwind classes (text-text, bg-background) reference CSS variables that switch values between light and dark mode automatically, without any Tailwind-level dark mode configuration changes.


Key Takeaways

  • Audit accessibility at the semantic token tier (Tier 2) — this gives full coverage with a small, manageable set of pairs instead of exponentially many component-level combinations.
  • Every semantic foreground token must be explicitly paired with its background token, and the contrast ratio of that pair must be measured and documented as part of the token definition.
  • Dark mode is not light mode inverted — always perform a separate, independent contrast audit for dark mode, and define dark mode token values through direct measurement rather than algorithmic inversion.
  • CI-enforced contrast checks on token pairs prevent future regressions — a token change that breaks contrast will fail the build before it reaches production.
  • Use OKLCH or HSL to build your primitive scale so that lightness increments map predictably to perceptual contrast, making it straightforward to identify accessible foreground/background step combinations.
  • Embed measured contrast ratios directly in token descriptions or documentation so every designer and developer working with the system can see at a glance which values have been audited and at what level they pass.