CSS Color Variables: Build a Scalable Design Token System
Learn how to use CSS custom properties for colors, structure a design token system, implement dark mode, and maintain consistency at scale.
- Define once in :root, reference everywhere with var()
- Use three layers: primitive → semantic → component tokens
- Dark mode is one @media or data-theme swap away
- Never encode hex values in token names
The Basics
CSS custom properties (CSS variables) are defined with a double-dash prefix and used via var(). They cascade like any CSS property and update live when changed via JavaScript:
:root {
--color-brand: #4f6ef7;
--color-text: #1a1a1a;
--color-bg: #ffffff;
}
button {
background: var(--color-brand);
color: var(--color-text);
}
A Three-Layer Token System
Raw hex values are primitives. Semantic tokens are what makes a system maintainable:
- Primitive tokens — Actual values:
--blue-500: #3b82f6 - Semantic tokens — Roles mapped to primitives:
--color-action: var(--blue-500) - Component tokens — Specific usage:
--button-bg: var(--color-action)
This three-layer approach lets you swap an entire theme by reassigning semantic tokens — without touching any components.
Dark Mode with CSS Variables
:root {
--bg: #ffffff;
--text: #1a1a1a;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #111111;
--text: #f5f5f5;
}
}
Or use a data-theme attribute for user-toggled switching:
[data-theme="dark"] { --bg: #111; --text: #f5f5f5; }
Naming Conventions
Consistent naming prevents team confusion. Common patterns:
--color-[role]-[variant]— e.g.,--color-surface-raised--[component]-[property]— e.g.,--button-border-radius- Scale-based:
--gray-100through--gray-900
Don't encode the hex in the name (--blue-color). When you change the value, the name becomes misleading.
Fallback Values
var() accepts an optional fallback: var(--color-brand, #4f6ef7). Use this in component libraries so they work even if the consuming project hasn't defined all tokens.
Export Workflow
Define your palette in a design tool → export to CSS variables → treat that file as single source of truth. React, Vue, Svelte, and plain HTML all consume the same token file.
Export your palette as CSS variables
Generate production-ready CSS custom properties with roles and dark mode