CSS Color Variables: The Complete Developer's Guide to Design Tokens
· Lion Dada

The Moment That Changed Everything
I still remember the exact moment CSS variables clicked for me. It was 2 AM, three weeks into a massive redesign project, and the client had just sent an email: "Actually, can we try a deeper blue for the brand color?"
In the old days—and by old days I mean like 2018—this would have meant hours of find-and-replace across dozens of files. Hunting down every #3B82F6 and swapping it for #2563EB. Missing some. Breaking things. Crying a little.
But that night, I changed one line of code:
--color-primary: #2563EB;
Refreshed. Done. The entire site updated. Every button, every link, every accent—all transformed in an instant.
That's when I realized: CSS variables aren't just a convenience. They're the foundation of professional color systems. And if you're not using them, you're working way harder than you need to.
Let me show you everything I wish someone had told me years ago.
What Are CSS Custom Properties?
CSS custom properties—commonly called CSS variables—are entities defined by developers that contain specific values to be reused throughout a document.
Here's the basic syntax:
:root {
--color-primary: #7241FF;
--color-secondary: #10B981;
--color-background: #FFFFFF;
--color-text: #1E293B;
}
.button {
background-color: var(--color-primary);
color: var(--color-background);
}
The :root selector targets the document's root element (<html>), making variables globally available. The -- prefix is required—it's what tells CSS "this is a custom property."
Using Variables with var()
You reference variables using the var() function:
.card {
background: var(--color-background);
border: 1px solid var(--color-border);
color: var(--color-text);
}
Fallback Values
Here's something many developers miss—you can provide fallback values:
.element {
/* If --color-accent isn't defined, use this blue */
background: var(--color-accent, #3B82F6);
}
This is crucial for defensive coding. Your styles won't break if a variable is accidentally undefined.
Browser Support
Great news: CSS variables have 97%+ browser support. Even IE11 workarounds exist if you're stuck supporting legacy browsers (my condolences).
For modern projects, you can use CSS variables without any polyfills or build tools. They just work.

CSS Variables vs. Sass/LESS Variables
"But I already use Sass variables. Why should I care about CSS variables?"
Great question. Here's the fundamental difference:
| Feature | Sass Variables | CSS Variables |
|---|---|---|
| When resolved | Compile-time | Runtime |
| Can change dynamically | ❌ No | ✅ Yes |
| Accessible in JavaScript | ❌ No | ✅ Yes |
| Works in any CSS | ❌ Needs build | ✅ Native |
| Theming capabilities | Limited | Powerful |
Sass variables are replaced with their values during compilation. Once your CSS is built, those variables no longer exist—they're just hardcoded values.
CSS variables exist at runtime. You can:
- Change them with JavaScript
- Override them with media queries
- Scope them to specific elements
- Toggle entire themes instantly
The Migration Path
You don't have to choose one or the other. Many teams use both:
// Sass variables for static values and calculations
$spacing-unit: 8px;
$border-radius-base: 4px;
// CSS variables for themeable values
:root {
--color-primary: #{$brand-primary};
--spacing-md: #{$spacing-unit * 2};
--radius-lg: #{$border-radius-base * 2};
}
Use Sass for calculations and compile-time logic. Use CSS variables for anything that needs to change dynamically.
Naming Conventions: The Secret to Scalable Systems
Here's where most developers go wrong: they name variables after what the color looks like instead of what it does.
/* ❌ Bad: Named by appearance */
--blue: #3B82F6;
--dark-blue: #1E40AF;
--light-blue: #DBEAFE;
/* ✅ Good: Named by function */
--color-primary: #3B82F6;
--color-primary-hover: #1E40AF;
--color-primary-subtle: #DBEAFE;
Why does this matter? Because when the rebrand happens (and it always happens), you don't want to have --blue: #22C55E in your codebase. That's just confusing.
The Three-Tier Naming System
Professional design systems use three levels of abstraction:
Tier 1: Primitive Colors (What it looks like)
--color-blue-50: #EFF6FF;
--color-blue-100: #DBEAFE;
--color-blue-500: #3B82F6;
--color-blue-900: #1E3A8A;
Tier 2: Semantic Colors (What it means)
--color-primary: var(--color-blue-500);
--color-primary-hover: var(--color-blue-600);
--color-success: var(--color-green-500);
--color-error: var(--color-red-500);
Tier 3: Component Colors (Where it's used)
--button-bg: var(--color-primary);
--button-bg-hover: var(--color-primary-hover);
--card-border: var(--color-neutral-200);
--input-focus-ring: var(--color-primary);
This hierarchy gives you flexibility at every level. Want to change the primary color? Update one variable. Want to change just button backgrounds? Also one variable. It's variables all the way down.
Real-World Naming Systems
Here's how the pros do it:
Tailwind CSS:
--color-primary-50 through --color-primary-950
--color-gray-50 through --color-gray-950
Material Design:
--md-sys-color-primary
--md-sys-color-on-primary
--md-sys-color-primary-container
IBM Carbon:
--cds-interactive-01
--cds-text-01
--cds-ui-background
Pick a convention and stick with it. Consistency beats cleverness.
Building a Complete Color System
Let's build a professional color system from scratch. Start with your brand colors, then expand systematically.
Step 1: Define Your Core Palette
:root {
/* Brand colors */
--color-brand-primary: #7241FF;
--color-brand-secondary: #10B981;
--color-brand-accent: #F59E0B;
/* Neutrals */
--color-neutral-50: #F8FAFC;
--color-neutral-100: #F1F5F9;
--color-neutral-200: #E2E8F0;
--color-neutral-300: #CBD5E1;
--color-neutral-400: #94A3B8;
--color-neutral-500: #64748B;
--color-neutral-600: #475569;
--color-neutral-700: #334155;
--color-neutral-800: #1E293B;
--color-neutral-900: #0F172A;
--color-neutral-950: #020617;
}
Step 2: Generate Shades Programmatically
For brand colors, you'll want multiple shades. Here's a technique using HSL:
:root {
/* Primary color in HSL for easy manipulation */
--primary-h: 258;
--primary-s: 100%;
--primary-l: 63%;
/* Generated shades */
--color-primary-50: hsl(var(--primary-h), var(--primary-s), 97%);
--color-primary-100: hsl(var(--primary-h), var(--primary-s), 94%);
--color-primary-200: hsl(var(--primary-h), var(--primary-s), 86%);
--color-primary-300: hsl(var(--primary-h), var(--primary-s), 76%);
--color-primary-400: hsl(var(--primary-h), var(--primary-s), 70%);
--color-primary-500: hsl(var(--primary-h), var(--primary-s), var(--primary-l));
--color-primary-600: hsl(var(--primary-h), var(--primary-s), 53%);
--color-primary-700: hsl(var(--primary-h), var(--primary-s), 43%);
--color-primary-800: hsl(var(--primary-h), var(--primary-s), 33%);
--color-primary-900: hsl(var(--primary-h), var(--primary-s), 23%);
}
Step 3: Create Semantic Mappings
:root {
/* Semantic colors */
--color-background: var(--color-neutral-50);
--color-foreground: var(--color-neutral-900);
--color-muted: var(--color-neutral-500);
--color-border: var(--color-neutral-200);
/* Interactive states */
--color-primary: var(--color-primary-500);
--color-primary-hover: var(--color-primary-600);
--color-primary-active: var(--color-primary-700);
/* Feedback colors */
--color-success: #10B981;
--color-warning: #F59E0B;
--color-error: #EF4444;
--color-info: #3B82F6;
}
Want to build your color system faster? Our Color Hierarchy tool helps you visualize and balance color proportions using the 60-30-10 rule.

Dark Mode Made Easy
CSS variables make dark mode almost trivial. Here are two approaches:
Approach 1: Media Query
:root {
--color-background: #FFFFFF;
--color-foreground: #1E293B;
--color-primary: #7241FF;
--color-muted: #64748B;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: #0F172A;
--color-foreground: #F8FAFC;
--color-primary: #9B7DFF;
--color-muted: #94A3B8;
}
}
This respects the user's system preference automatically.
Approach 2: Data Attribute (Manual Toggle)
:root {
--color-background: #FFFFFF;
--color-foreground: #1E293B;
}
[data-theme="dark"] {
--color-background: #0F172A;
--color-foreground: #F8FAFC;
}
With JavaScript:
// Toggle dark mode
document.documentElement.setAttribute('data-theme', 'dark');
// Or combine with media query
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
The Best Practice: Both
:root {
color-scheme: light dark;
--color-background: #FFFFFF;
--color-foreground: #1E293B;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--color-background: #0F172A;
--color-foreground: #F8FAFC;
}
}
[data-theme="dark"] {
--color-background: #0F172A;
--color-foreground: #F8FAFC;
}
[data-theme="light"] {
--color-background: #FFFFFF;
--color-foreground: #1E293B;
}
This respects system preference by default but allows manual override. It's how most modern sites handle theming.
Need help converting your light palette to dark mode? Try our Light/Dark Mode Generator—it uses AI to create accessible dark mode colors automatically.

Design Tokens: Beyond CSS Variables
Design tokens are the next evolution of design systems. They're platform-agnostic values that represent design decisions.
While CSS variables are one output of design tokens, tokens themselves can generate:
- CSS custom properties
- iOS Swift constants
- Android XML resources
- JavaScript/TypeScript objects
- Figma styles
Token Formats
Style Dictionary (Amazon):
{
"color": {
"primary": {
"value": "#7241FF",
"type": "color"
},
"background": {
"value": "{color.neutral.50}",
"type": "color"
}
}
}
Design Token Community Group (DTCG) Format:
{
"color": {
"primary": {
"$value": "#7241FF",
"$type": "color"
}
}
}
The Tokens Pipeline
- Define tokens in Figma (using Tokens Studio plugin)
- Export to JSON format
- Transform using Style Dictionary
- Generate platform-specific code
This means one source of truth for colors across web, iOS, Android, and design tools.
Tools for Design Tokens
- Tokens Studio for Figma: Edit tokens directly in Figma
- Style Dictionary: Transform tokens to any format
- Chromatic: Visual testing for design systems
- Storybook: Document your component library

Extracting CSS Variables from Existing Sites
Here's where things get really interesting. You can learn from the best design systems by extracting their CSS variables.
Our Coloracci Chrome Extension makes this effortless:
- Visit any website
- Click the extension icon
- See all their CSS custom properties
- Copy individual values or export the entire color system
Why This Matters
Studying how successful sites structure their variables teaches you patterns you'd never think of yourself. When I first extracted Stripe's color system, I noticed they had variables I'd never considered—like --color-button-focus-ring-offset and --color-chart-4.
That's the difference between amateur and professional design systems.
Workflow: Extract → Analyze → Implement
- Extract CSS variables from 3-5 sites in your industry
- Analyze common patterns in naming and organization
- Implement the best ideas in your own system
This isn't copying—it's learning from established patterns. No one invents design systems in a vacuum.

Common Mistakes & How to Avoid Them
Mistake 1: Too Many Primitive Colors
/* ❌ Bad: Color for every possible shade */
--blue-1: #EFF6FF;
--blue-2: #DBEAFE;
--blue-3: #BFDBFE;
/* ... 50 more */
/* ✅ Good: Curated scale with clear steps */
--blue-50: #EFF6FF;
--blue-100: #DBEAFE;
--blue-200: #BFDBFE;
--blue-500: #3B82F6;
--blue-900: #1E3A8A;
You don't need 50 shades of blue. A well-designed 10-step scale covers 99% of use cases.
Mistake 2: Forgetting Fallback Values
/* ❌ Risky: No fallback */
.card {
background: var(--card-bg);
}
/* ✅ Safe: Has fallback */
.card {
background: var(--card-bg, var(--color-background, #FFFFFF));
}
Fallbacks prevent broken styles when variables are undefined.
Mistake 3: Not Scoping Variables
/* ❌ All global: Hard to manage */
:root {
--card-padding: 16px;
--card-radius: 8px;
--button-height: 40px;
}
/* ✅ Scoped appropriately */
:root {
--spacing-md: 16px;
--radius-md: 8px;
}
.card {
--card-padding: var(--spacing-md);
padding: var(--card-padding);
}
Mistake 4: Ignoring Accessibility in Dark Mode
Dark mode isn't just "invert the colors." You need to maintain contrast ratios:
/* ❌ Bad: Same contrast ratio */
:root {
--text-muted: #64748B; /* 4.54:1 on white */
}
[data-theme="dark"] {
--text-muted: #64748B; /* 1.78:1 on dark - FAILS WCAG */
}
/* ✅ Good: Adjusted for contrast */
[data-theme="dark"] {
--text-muted: #94A3B8; /* 5.22:1 on dark - Passes */
}
Use our Color Contrast Checker to verify your dark mode colors meet WCAG standards.
Mistake 5: Inconsistent Naming
Pick a convention and enforce it:
/* ❌ Inconsistent */
--primaryColor: #7241FF;
--secondary-color: #10B981;
--accent_color: #F59E0B;
/* ✅ Consistent: kebab-case */
--color-primary: #7241FF;
--color-secondary: #10B981;
--color-accent: #F59E0B;
Real-World Examples
Let's look at how leading companies structure their CSS variables:
Stripe
:root {
--accent-color: #635bff;
--accent-color-hover: #7a73ff;
--background-color: #ffffff;
--foreground-color: #1a1a1a;
--muted-color: #687076;
--border-color: #e6e6e6;
}
Simple, clear, effective. Stripe's system focuses on semantic meaning over implementation details.
Tailwind CSS
:root {
--color-primary: oklch(0.6 0.2 270);
--color-secondary: oklch(0.7 0.1 160);
--radius-sm: 0.125rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
}
Tailwind v4 uses OKLCH color space for perceptually uniform colors—a cutting-edge approach.
GitHub Primer
[data-color-mode="light"] {
--color-fg-default: #1F2328;
--color-fg-muted: #656D76;
--color-fg-subtle: #6E7781;
--color-canvas-default: #FFFFFF;
--color-canvas-subtle: #F6F8FA;
}
GitHub prefixes foreground colors with fg and background colors with canvas—a clever semantic distinction.
Advanced Techniques
CSS color-mix() with Variables
:root {
--color-primary: #7241FF;
}
.card:hover {
/* Mix primary with black for hover state */
background: color-mix(in srgb, var(--color-primary), black 10%);
}
.card-subtle {
/* Mix primary with white for subtle background */
background: color-mix(in srgb, var(--color-primary), white 90%);
}
Calculated Colors with HSL
:root {
--primary-h: 258;
--primary-s: 100%;
--primary-l: 63%;
}
.element {
/* Programmatically lighter version */
background: hsl(var(--primary-h), var(--primary-s), calc(var(--primary-l) + 20%));
}
Animation with Variables
:root {
--animation-duration: 0.3s;
--animation-timing: cubic-bezier(0.4, 0, 0.2, 1);
}
.button {
transition:
background-color var(--animation-duration) var(--animation-timing),
transform var(--animation-duration) var(--animation-timing);
}
Next Steps: Your Action Plan
Ready to level up your color management? Here's your roadmap:
- Audit your current CSS - How many hardcoded colors do you have?
- Create your variable structure - Use the three-tier naming system
- Implement basic theming - Add dark mode support
- Extract inspiration - Use our Chrome Extension to study great sites
- Build your palette - Try the Light/Dark Mode Generator for instant dark themes
Related Tools
- Color Hierarchy - Balance your color proportions
- AI Color Picker - Generate harmonious palettes
- Color Contrast Checker - Verify accessibility
- Image Color Extractor - Build palettes from images
Further Reading
- The Science Behind Color Proportions - Why 60-30-10 works
- Color Picker vs Extractor - Which tool for which job
- Dark Mode Design Colors - Complete dark mode guide
- Complementary Colors Examples - Color harmony fundamentals
CSS variables transformed how I build websites. No more hunting through files for color values. No more dreading "can we change the brand color?" requests. Just clean, maintainable, scalable color systems.
Your future self will thank you for learning this stuff today.
Want to extract CSS variables from any website? Get our free Chrome Extension and start learning from the best design systems on the web.