DESIGN SYSTEMS · TOKENS
Semantic color tokens: name the purpose, not the paint
A value called “dark gray” becomes a problem the moment the theme changes. A value called “text primary” still describes its job. Good token architecture separates raw colors from meaning and meaning from component decisions.
Appearance names are easy to create and hard to maintain
Names such as gray-700, brand-green, and light-border can be useful at the raw palette level. They become fragile when application code uses them directly.
Consider a dark theme. The token light-border may need to become darker than the page surface. The name now contradicts the value. A semantic name such as border-subtle continues to make sense because it describes the relationship the border should have, not its absolute appearance.
A stable token name describes a design decision that can survive a new theme, brand revision, or platform.
Use three layers when the system is large enough
| Layer | Purpose | Example |
|---|---|---|
| Primitive | Stores reusable raw values and scales | blue-600, neutral-950 |
| Semantic | Assigns values to product-wide roles | text-primary, surface-raised |
| Component | Resolves a specific component decision | button-primary-background-hover |
Not every small project needs all three layers. The separation becomes valuable when multiple themes, brands, platforms, or teams share the same system.
:root {
--blue-600: #1b6fd1;
--neutral-0: #ffffff;
--neutral-950: #111827;
--color-action-primary: var(--blue-600);
--color-on-action-primary: var(--neutral-0);
--color-text-primary: var(--neutral-950);
}
.button-primary {
color: var(--color-on-action-primary);
background: var(--color-action-primary);
}Semantic names should describe role and relationship
Useful semantic categories include:
- Surface: page, raised, inset, overlay, inverse
- Text: primary, secondary, muted, inverse, link, disabled
- Border: subtle, default, strong, focus, error
- Action: primary, secondary, destructive, selected
- Status: success, warning, error, information
- Data: categorical, sequential, diverging, emphasis
Avoid semantic names that are still secretly appearance names. text-gray does not explain whether the token is primary, secondary, disabled, or placeholder text.
Name paired roles together
When a background requires a specific foreground, make the relationship explicit:
action-primaryandon-action-primarysurface-inverseandtext-on-inversestatus-error-surfaceandstatus-error-text
This reduces the chance that a developer picks a readable background but pairs it with the wrong label color.
Themes should remap semantics, not rewrite components
A component should ask for the same semantic role in every theme. The theme decides which primitive satisfies that role.
:root,
[data-theme="light"] {
--color-surface-page: #ffffff;
--color-surface-raised: #f8fafc;
--color-text-primary: #111827;
}
[data-theme="dark"] {
--color-surface-page: #0b1220;
--color-surface-raised: #131d2e;
--color-text-primary: #f3f6fb;
}The card component still uses surface-raised and text-primary. It does not need to know whether the values are light or dark.
Treat accessible pairs as contracts
Semantic tokens are an opportunity to document which roles are designed to work together.
| Foreground token | Approved backgrounds | Target |
|---|---|---|
text-primary | Page, raised, inset | AA normal text or higher |
text-muted | Page, raised | AA at intended size |
on-action-primary | Action primary default, hover, active | AA normal text |
focus-ring | Every adjacent surface and component edge | Visible non-text contrast |
Automated tests can then check token pairs whenever values change. The token architecture becomes part of the accessibility safety net rather than a naming exercise.
Add component tokens only when they express a real decision
Component tokens are useful when a component has a deliberate choice that should not be inferred from a generic semantic role.
A button may need:
- Background default, hover, active, and disabled
- Label default and disabled
- Border default and focus
- Icon color
- Loading indicator
Do not create a component token for every CSS property automatically. Too many aliases hide the system rather than clarifying it. Add one when the component needs independence, a documented contract, or a stable customization point.
Migrate without breaking every consumer
- Inventory existing values.Use code search or Website Scanner to identify repeated and near-duplicate colors.
- Create primitives.Normalize the values that genuinely belong in the palette.
- Define semantic roles.Map current use cases rather than guessing from color values alone.
- Add aliases.Temporarily point old variables to new semantic tokens.
- Move components gradually.Start with high-use components and critical accessibility pairs.
- Deprecate old names.Document the replacement and removal timeline.
- Test every theme.Run visual, contrast, and interaction checks before removing aliases.
Token-system checklist
- Primitive values are separate from semantic meaning.
- Semantic names describe purpose, hierarchy, or relationship.
- Foreground and background companions are documented.
- Every theme remaps the same semantic roles.
- Critical token pairs have automated contrast checks.
- Component tokens exist only for real component decisions.
- Disabled, focus, hover, active, and error states are explicit.
- Token files are versioned and changes are documented.
- Old aliases have a migration and removal plan.
- Exports use the naming conventions of the consuming design and code systems.
Continue learning
The HexCheck export guide covers CSS, JSON, Tailwind, Figma, and token validation. Use Website Scanner to inventory public CSS and Brand Kits to keep semantic colors and typography together.
GO DEEPER
The color science behind semantic tokens and color behavior.
Connect token architecture to perceptual spacing, appearance under changing contexts, and cross-display color management.