CSS Custom Properties for Color Values: Design Tokens in Practice
CSS custom properties (also called CSS variables) give you a way to define reusable values that update dynamically across your entire stylesheet. For color management, they are the foundation of design token systems, dark mode support, and consistent theming — all without a preprocessor.
Why Custom Properties for Colors
Hard-coded color values scattered through a stylesheet create maintenance problems. When your brand blue changes from #3B82F6 to #2563EB, you must find and replace every instance. With custom properties, you change one definition:
:root {
--color-primary: #3B82F6;
}
.button {
background-color: var(--color-primary);
}
.heading {
color: var(--color-primary);
}
Update --color-primary once, and every element using it updates automatically. This is particularly powerful for dark mode — you redefine the variables under a different selector rather than overriding individual properties.
Setting Up a Color Token System
A well-structured color token system has three layers: primitive tokens, semantic tokens, and component tokens.
/* Layer 1: Primitive tokens — raw color values */
:root {
--blue-500: #3B82F6;
--blue-600: #2563EB;
--blue-700: #1D4ED8;
--gray-100: #F3F4F6;
--gray-900: #111827;
--red-500: #EF4444;
--green-500: #22C55E;
}
/* Layer 2: Semantic tokens — meaning-based aliases */
:root {
--color-primary: var(--blue-500);
--color-primary-hover: var(--blue-600);
--color-bg: var(--gray-100);
--color-text: var(--gray-900);
--color-danger: var(--red-500);
--color-success: var(--green-500);
}
/* Layer 3: Component tokens — specific to UI elements */
:root {
--button-bg: var(--color-primary);
--button-text: white;
--card-bg: white;
--card-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
Primitive tokens should rarely appear outside the :root definition. Components reference semantic tokens, and semantic tokens reference primitives. This separation makes rebranding or theming a configuration change, not a refactor.
Dark Mode With Custom Properties
Custom properties make dark mode almost trivial. Override semantic tokens under your dark mode selector:
.dark {
--color-bg: var(--gray-900);
--color-text: var(--gray-100);
--color-primary: var(--blue-400);
--card-bg: var(--gray-800);
--card-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
When the .dark class is applied to the <html> element, every semantic token re-resolves to its dark-mode value. Components that reference var(--color-bg) automatically receive the dark background. No component code changes required.
This pattern works with @media (prefers-color-scheme: dark) as well:
@media (prefers-color-scheme: dark) {
:root {
--color-bg: var(--gray-900);
--color-text: var(--gray-100);
}
}
For sites using @nuxtjs/color-mode, the module manages the class switching automatically.
Color Format Considerations
Custom properties store color values as strings. The format you choose affects what you can do with the value downstream:
| Format | Alpha Support | Composable | Example |
|---|---|---|---|
| Hex | Limited (8-digit) | No | #3B82F6 |
| RGB | Yes, via rgba() | Yes | rgb(59, 130, 246) |
| HSL | Yes, via hsla() | Yes | hsl(217, 91%, 60%) |
| OKLCH | Yes, native | Yes | oklch(0.62 0.19 264) |
| Color-mix | Full | Yes | color-mix(in srgb, var(--color-primary) 80%, white) |
If you need to adjust opacity dynamically, RGB or HSL formats work better than hex:
:root {
--color-primary-rgb: 59, 130, 246;
}
.overlay {
background-color: rgba(var(--color-primary-rgb), 0.5);
}
The modern color-mix() function eliminates the need for separate RGB variables:
.button:hover {
background-color: color-mix(in srgb, var(--color-primary) 85%, white);
}
Fallback Values
Custom properties support fallback values when a variable is not defined:
.badge {
color: var(--badge-color, var(--color-danger));
}
Use fallbacks for component tokens that may not always be defined. This keeps components functional even when a specific theme does not override the token.
Key Takeaways
- CSS custom properties centralize color definitions — change one value, update the entire theme
- Structure color tokens in three layers: primitive → semantic → component
- Dark mode becomes a selector-based variable override with zero component changes
- Choose RGB/HSL over hex when you need composable alpha values
- Use
color-mix()for dynamic color manipulation without JavaScript - Always provide fallback values for optional component tokens
Related Guides
- Hex to RGB: Understanding Color Format Conversion
- Color Models Explained: RGB, HSL, HEX, and More
- CSS Color Values: A Complete Reference
Try It Yourself
Convert hex colors to RGB for composable custom properties with our free Hex to RGB converter. Enter any hex value and get RGB, HSL, and OKLCH formats ready for your design token system.