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-primaryon--color-surface-page--color-text-secondaryon--color-surface-page--color-text-primaryon--color-surface-raised--color-text-on-actionon--color-action-primary--color-linkon--color-surface-page--color-text-on-statuson status backgrounds--color-focus-ringaround 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:
| Foreground | Background | Intended use | Decision |
|---|---|---|---|
| primary text | page surface | headings and body copy | approve after testing |
| secondary text | raised surface | supporting copy | approve only if readable at its size |
| text on action | primary action | button label | approve for every action state |
| muted text | page surface | metadata | limit to nonessential copy |
| link | page surface | navigation and inline links | test 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:
- Default, hover, pressed, and disabled buttons.
- Empty, filled, invalid, and focused inputs.
- Links on page and raised surfaces.
- Selected, checked, expanded, and active controls.
- Success, warning, and error messages.
- Text over gradients, images, or tinted panels.
- 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.