Semantic Color Tokens — Role-Based Color Naming
Why naming colors by their role beats naming them by hue — and how to structure a semantic token system that scales.
The Problem With Hue-Based Names
Most design systems start with hue-based names: --blue, --red, --green. This seems natural — blue is blue, red is red. But it creates a cascade of problems:
- Rebranding breaks everything: If you rename the primary brand color from blue to purple, every reference to
--bluemust change - Dark mode is impossible: Blue at night is not the same as blue in daylight — you need different values, not just hue names
- Semantic meaning is lost: "Use --blue" doesn't tell you why you are using blue
- Multiple blues conflict: Are you referring to the brand blue, the link blue, the info blue?
What Semantic Naming Solves
Semantic tokens are named by role: --color-primary, --color-text, --color-danger. This means:
- Change is centralized: Redefine
--color-primaryonce, everything updates - Theming works: Override semantic tokens for dark mode, brand variants, white-labeling
- Intent is clear: "Use --color-primary" tells you it's a primary action
- No ambiguity: One token, one meaning, one use
The Token Hierarchy
Primitives (Values)
Raw values: --blue-500: #155EEF. Never used directly in components.
Semantics (Intent)
Role-based: --color-primary: var(--blue-500). What components use.
Components (Scoping)
Optional: --button-bg: var(--color-primary). Used for component-specific overrides.
Standard Semantic Token Categories
Brand Tokens
--color-primary--color-secondary--color-accent
Interactive State Tokens
--color-primary-hover--color-primary-active--color-primary-focus--color-primary-disabled
Surface Tokens
--color-background--color-surface--color-surface-elevated--color-border
Text Tokens
--color-text--color-text-muted--color-text-subtle--color-text-inverse
System State Tokens
--color-success+--color-success-light--color-warning+--color-warning-light--color-danger+--color-danger-light--color-info+--color-info-light
Naming Rules
- Role first:
color-textnottext-color - Consistent structure:
--{category}-{role}-{variant} - No hue names: Never
--blue-button - No camelCase: Kebab-case for CSS compatibility
- No numbers except scale:
--color-primarynot--color-primary-2
Theming With Semantic Tokens
The biggest benefit: theming becomes trivial. To add dark mode, override semantic tokens without touching primitives or components:
[data-theme="dark"] {
--color-background: #0F172A;
--color-surface: #1A2437;
--color-text: #F1F5F9;
--color-border: #2D3A52;
}
/* Components automatically inherit the new colors */
Common Mistakes
- Creating too many tokens: Every component gets its own tokens — creates a maintenance nightmare
- Using primitives directly:
background: var(--blue-500)bypasses theming - Inconsistent naming: Mixing
--color-primaryand--primary-color - Forgetting dark mode: Semantic tokens without dark overrides fail when you need to add theming
- Reusing tokens for different meanings: Using
--color-primaryfor both brand and success undermines semantics
Building Your Semantic System
- Audit your current color usage
- Identify semantic roles (what each color communicates)
- Map primitives to semantic tokens
- Document the mapping
- Refactor components to use semantic tokens only
- Set up theming via semantic overrides
Generate Your Tokens
Use our CSS Variables Generator to auto-generate 10 semantic tokens from any base color.