Skip to content
Let’s chat ↗
THE TDP FIELD GUIDE

How to create semantic color tokens for UI

Semantic color tokens name what a color does, such as text-primary or surface-error. Map them to primitive values, document where they belong, and validate each theme and state. The same semantic name can resolve to a different value in light and dark modes.

On this page · 14 sections
WHAT YOU’LL MAKE

A role-based token map for surfaces, text, actions, and feedback.

Before you start

  • An existing palette or the palette guide
  • A representative interface with feedback states
  • The token format used by your implementation

Why semantic names matter

Semantic color tokens describe what a color does rather than only what it looks like. A name such as text-secondary or surface-error gives a contributor a decision rule. A name such as gray-500 identifies a value in a palette but does not explain where it belongs.

In TDP’s Figma-to-Storybook demonstration, the token audit checks whether semantic colors actually reference primitives, identifies missing mappings, and revisits the results visually. That sequence matters: a token can have a semantic-looking name while still containing an unmanaged raw value.

Use this guide to connect a palette to one real interface. The examples are intentionally small so you can inspect the complete path from primitive value to component use.

Step 1. Inventory colors and their jobs

List current colors alongside the elements that use them. Group by role before grouping by identical values. A destructive action and a brand accent may share a red today but should remain separately named if they communicate different things. This preserves meaning when the brand or theme changes.

Step 2. Separate primitive values from semantic roles

A primitive describes a value in a palette; a semantic token describes its purpose. Your implementation may use CSS, JSON, or a design-token tool. Choose the format your team can maintain and avoid inventing a parallel source of truth.

:root {
  --neutral-950: #242424;
  --neutral-50: #faf7f4;
  --red-800: #8c2538;
  --surface-default: var(--neutral-50);
  --text-primary: var(--neutral-950);
  --text-error: var(--red-800);
}
[data-theme="dark"] {
  --surface-default: #242424;
  --text-primary: #faf7f4;
  --text-error: #ffb7c0;
}

Step 3. Specify states and pairings

Document which foreground belongs on which surface. Add state tokens when an interaction needs them, and state whether a border is decorative or necessary to identify the control. Do not assume a darker hover color or a lighter disabled color remains legible in every theme.

RolePair to reviewMeaning
Primary textText on default and raised surfacesMain reading content
Action textLabel on action backgroundWhat the button will do
Error textMessage on error or default surfaceWhat failed and how to recover
FocusIndicator beside adjacent colorsCurrent keyboard position

Step 4. Add a non-color explanation

An error needs text or another meaningful cue, not just a red border. A chart series may need labels, shapes, or patterns. W3C’s use-of-color guidance explains why color alone is insufficient for conveying information. Design the message and color together instead of adding a cue at the end.

Step 5. Migrate a small slice

Replace values in one component family, inspect every supported theme, and review the result in a full page. Record intentional exceptions. Search for remaining raw values and classify them before replacing them; some may belong to imagery or a context outside the shared token system.

Worked example: separate a value from its job

Imagine the same blue appears on the primary button and a selected row. Those roles happen to share a primitive today, but they can change independently later. Keep the semantic names distinct so a future selected-state adjustment does not automatically recolor every action.

:root {
  --palette-blue-700: #1d4ed8;
  --palette-blue-50: #eff6ff;
  --color-action-primary: var(--palette-blue-700);
  --color-selection-surface: var(--palette-blue-50);
  --color-selection-text: var(--palette-blue-700);
}

These are illustrative CSS variables, not a complete theme or an interchange-format example. Validate the pairs in your interface and use your project’s naming and build conventions.

Audit aliases without erasing intent

When two semantic tokens share a value, ask whether they share a responsibility. Field borders and decorative dividers may look identical in one theme while serving different purposes. A blanket deduplication can make later accessibility or theme work harder.

FindingWhat it might meanReview action
Semantic token stores a raw valueAlias was never connectedConfirm the primitive and replace the mapping
Two roles share a primitiveIntentional reuseKeep both names if their responsibilities differ
A role has no dark-mode mappingIncomplete theme supportDefine and test the missing pair
A tinted value uses opacityAppearance depends on the backgroundInspect the composite result
A component uses a primitive directlyMissing or bypassed semantic roleDecide whether a role is needed

Use the Design Tokens format reference when exchanging typed token data between tools. Your runtime CSS can remain a separate generated or maintained representation. Do not imply that a project-specific JSON object automatically conforms to that format.

Document foregrounds and surfaces together

A background token alone is incomplete guidance for a filled control or feedback message. Document the intended foreground, border where relevant, and interaction states. A consumer should not have to improvise a label color for every surface.

The same principle applies to themes. Keep the role stable while allowing its value to change. Review the role in the same component across supported modes, including focus and error presentation. A token name cannot guarantee the resulting pair is readable.

Introduce the map through one component

Replace the approved values in a small component, then compare its states before and after. Inspect the implementation for raw values and verify that changing the semantic mapping reaches the intended consumers. This catches aliases that exist in documentation but are not actually used.

Keep changes reviewable. Migrating every color in the product at once makes it difficult to identify which mapping caused a regression. Preserve a record of unresolved values and expand once the pilot works.

Maintain semantic roles as product decisions

Create a new role when it expresses a meaningful responsibility, not merely because a new screen needs a slightly different shade. Ask whether an existing role fits and whether the exception belongs at the component level. Explain intentional exceptions so agents and teammates do not copy them indiscriminately.

For the palette itself, use create a UI color palette. For the larger relationship between tokens, components, and agent context, use build an AI-ready design system.

Completion checklist

  • Semantic names describe intent.
  • Theme mappings preserve meaning.
  • Foreground/background pairs are documented.
  • Status includes a cue beyond hue.
  • Migration is checked in component and page context.

Common questions

Should two identical values share one semantic token?

Only when they share a role. Separate roles can reference the same primitive without becoming the same token.

Is every border required to meet 3:1 contrast?

No. W3C’s non-text contrast criterion depends on whether the visual information is needed to identify a component or state. Evaluate the boundary’s purpose and the criterion’s exceptions.

Sources and TDP resources

TRY IT HERE

Check a color pair

Use opaque six-digit hex values, such as #2e2e2e. This tool does not evaluate gradients, transparency, or full accessibility conformance.

Make the next step clear.

Check the text against the surface it actually sits on.

12.59:1AA ordinary text (4.5:1): Pass
AA large text (3:1): Pass
Non-text numeric reference (3:1): Meets threshold

Decisions use the unrounded result. Large text: at least 18pt, or 14pt bold. UI applicability and exceptions need a separate review.

color: #2e2e2e;
background-color: #fcf5f1;