The Problem CSS Custom Properties Solve
Before CSS custom properties (commonly called CSS variables), maintaining color consistency across a large web application meant duplicating hex codes throughout thousands of lines of CSS, depending on Sass or Less preprocessing, or using a CSS-in-JS solution that added JavaScript overhead to what should be a pure styling concern.
Introduced in the CSS Custom Properties Level 1 specification and supported in all modern browsers since 2016, CSS custom properties enable true runtime design tokens — values that cascade, can be overridden in local scope, respond to media queries, and are fully accessible via JavaScript without style recalculation.
Understanding the Token Hierarchy
A well-structured color token system operates across three semantic layers:
Layer 1: Raw Scale Tokens
These are the mathematical outputs of your color scale generator — every step of every hue in your palette. They are named descriptively and serve as the vocabulary from which higher-level tokens are drawn:
:root {
/* Blue Scale */
--color-blue-50: #eff6ff;
--color-blue-100: #dbeafe;
--color-blue-200: #bfdbfe;
--color-blue-300: #93c5fd;
--color-blue-400: #60a5fa;
--color-blue-500: #3b82f6;
--color-blue-600: #2563eb;
--color-blue-700: #1d4ed8;
--color-blue-800: #1e40af;
--color-blue-900: #1e3a8a;
--color-blue-950: #172554;
}
Layer 2: Semantic Decision Tokens
These tokens assign meaning to the raw scale values. They describe intent rather than appearance — --color-action rather than --color-blue-600. This is where your design decisions live:
:root {
/* Semantic assignments — light mode */
--bg: var(--color-slate-50);
--surface: #ffffff;
--text: var(--color-slate-900);
--text-muted: var(--color-slate-400);
--border: var(--color-slate-200);
--action: var(--color-blue-600);
--action-hover: var(--color-blue-700);
--action-text: #ffffff;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: var(--color-slate-900);
--surface: #1e293b;
--text: var(--color-slate-100);
--text-muted: var(--color-slate-400);
--border: #334155;
--action: var(--color-blue-400);
--action-hover: var(--color-blue-300);
--action-text: var(--color-slate-900);
}
}
Layer 3: Component-Level Tokens
The most granular layer allows specific components to override semantic tokens within their scope using CSS custom property inheritance:
.card {
--surface: var(--color-slate-50);
background: var(--surface);
border: 1px solid var(--border);
}
.btn--success {
--action: var(--success);
--action-hover: #047647;
}
.btn--success:is(:hover, :focus-visible) {
background: var(--action-hover);
}
Naming Convention Best Practices
| Anti-Pattern | Why It Breaks Down | Better Alternative |
|---|---|---|
--blue |
Not descriptive — blue might become teal in a rebrand | --color-action or --brand-primary |
--button-color |
Too component-specific for a global token | --action (shared by buttons, links, checkboxes) |
--lightblue |
No scale context — is this the 100 or the 300? | --color-blue-200 |
--text1, --text2 |
Opaque numbering with no semantic meaning | --text, --text-muted, --text-disabled |
Accessing CSS Variables in JavaScript
Unlike Sass variables which are compiled away, CSS custom properties are live in the browser CSSOM. JavaScript can read and write them at runtime without triggering full style recalculation:
// Read a CSS custom property value
const root = document.documentElement;
const actionColor = getComputedStyle(root)
.getPropertyValue('--action')
.trim();
// Override a token at runtime (e.g. user theme picker)
root.style.setProperty('--action', '#7c3aed');
// Remove the override, restoring the stylesheet default
root.style.removeProperty('--action');
This pattern powers runtime theme customization, per-user color preference saving, and dynamic design token preview tools — all without a single full-page reload. Pair this technique with our Shades Generator to generate complete token scales, and our HEX to RGB Converter to translate brand hex codes into HSL values ideal for custom property use.