Turn Text and Background Contrast Into Reusable Design Tokens

Map text and background contrast into named tokens so accessible pairings stay consistent across dark mode surfaces, components, states, and themes.

turn text and background contrast into reusable design tokens

Contents

  • [Start with semantic pairings](#start-with-semantic-pairings)
  • [Make a contrast decision table](#make-a-contrast-decision-table)
  • [Test states, not just the default screen](#test-states-not-just-the-default-screen)
  • [Keep contrast decisions maintainable](#keep-contrast-decisions-maintainable)
  • [Use this in your AI agent](#use-this-in-your-ai-agent)

Turn text and background contrast into reusable tokens by naming each pairing according to its job, then checking the same pairing wherever it appears. The goal is not to save a list of passing hex combinations; it is to make safe foreground and background choices repeatable in buttons, cards, forms, links, and states.

Start with semantic pairings

Create tokens for foreground roles and surface roles, then define approved combinations between them:

  • --color-text-primary on --color-surface-page
  • --color-text-secondary on --color-surface-page
  • --color-text-primary on --color-surface-raised
  • --color-text-on-action on --color-action-primary
  • --color-link on --color-surface-page
  • --color-text-on-status on status backgrounds
  • --color-focus-ring around controls and fields

This approach is stronger than naming values by shade. A token called --gray-400 does not tell a designer whether it is safe for body text, a border, or a button label. A token called --color-text-secondary gives the team a clear purpose, while the contrast check determines where that role can be used.

The examples below show a green direction with observed values including #15502e, #254f1a, #556659, #4a5a4a, and #799c92. Compare those examples for visual direction, but verify every foreground and background pairing in the interface where you plan to use it.

A CSS map can make the intended relationships visible:

Captured pages

Colors

  • #496c10
  • #15502e
  • #14532d
  • #233f2a
  • #254f1a
  • #1e6f30
  • #295631
  • #556659
  • #4a5a4a
  • #2c7a4a
  • #2a966f
  • #799c92

Make a contrast decision table

For each pairing, record four things: the foreground token, the background token, the content type, and the allowed use. For example:

ForegroundBackgroundIntended useDecision
primary textpage surfaceheadings and body copyapprove after testing
secondary textraised surfacesupporting copyapprove only if readable at its size
text on actionprimary actionbutton labelapprove for every action state
muted textpage surfacemetadatalimit to nonessential copy
linkpage surfacenavigation and inline linkstest color and another visual cue

Do not use one token for every low-emphasis element. Muted text, disabled text, borders, and decorative marks have different communication jobs. A color that works as a border may be too weak for a sentence, and a bright focus ring may be distracting as ordinary body text.

Test states, not just the default screen

Review contrast in the states visitors actually use:

  1. Default, hover, pressed, and disabled buttons.
  2. Empty, filled, invalid, and focused inputs.
  3. Links on page and raised surfaces.
  4. Selected, checked, expanded, and active controls.
  5. Success, warning, and error messages.
  6. Text over gradients, images, or tinted panels.
  7. Focus indicators against both the control and its surrounding surface.

If a component needs a special pairing, create a component token that points to the closest semantic role. For instance, --button-primary-text can reference --color-text-on-action, while --button-primary-text-disabled can use a separate approved value. This avoids silently reusing a token in a state it was never meant to support.

Keep contrast decisions maintainable

Store contrast metadata beside the token map. A useful record includes the pair name, foreground token, background token, content category, test result, and the components allowed to use it. When a primitive color changes, retest every semantic pairing that depends on it.

Use Fudge to inspect how colors are used across a captured page when you need to distinguish text, surfaces, borders, and accents. The green examples below are useful for comparing visual relationships, but they do not replace checks on your own page, font sizes, weights, or interaction states.

A good review checklist is simple: every text token has an approved surface, every action token has an approved label, every state token has a clear meaning, and every exception is documented rather than copied into another component.

Use this in your AI agent

> Inspect this captured page's text and background colors and turn the observed contrast relationships into semantic design tokens. List each foreground and surface value, propose names based on use, map approved pairings for headings, body text, secondary text, links, buttons, inputs, borders, focus, and status states, and flag pairings that need a contrast check. Return CSS variables and a compact JSON map. Do not treat an isolated swatch as proof that every usage is accessible. > > Install Fudge for your AI agent

How do I organize contrast tokens so they work in both light and dark mode?

Keep the role names identical across themes and swap the values by theme. The component should ask for --color-text-primary on --color-surface-page, not for a light-specific or dark-specific shade. This lets the same component structure work in both modes.

For each theme, maintain a pairing table that includes foreground, background, content type, and approved components. A pairing may be valid in dark mode but need a different primitive value in light mode. That is fine as long as the semantic role stays stable.

Use component tokens only when the role needs a state-specific exception. For example, --button-primary-text may point to the shared action-label token, while --button-primary-text-disabled points to a disabled-state token. Retest the table whenever a primitive value, font size, weight, border, or background changes.

What is the fastest practical way to audit contrast before release?

Audit the highest-risk combinations first. Open a page that contains the main background, raised panels, primary and secondary text, links, buttons, forms, alerts, and focus states. Check each pairing at its actual size and weight, then repeat for hover, pressed, disabled, selected, invalid, and focused states.

Record failures by token name rather than by component screenshot. If several components fail because --color-text-muted is too faint, fix the token or narrow its allowed use instead of patching every component separately. If only one component fails, create a component-level exception and document why.

Also review text placed over images, gradients, and tinted surfaces. Keep a small regression page with these examples so future palette changes can be checked quickly. The page should make it obvious which combinations are approved, conditional, or rejected.