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

· Lion Dada

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

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 naming hierarchy diagram

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:

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.

Light and dark mode comparison

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 workflow from Figma to CSS

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

  1. Define tokens in Figma (using Tokens Studio plugin)
  2. Export to JSON format
  3. Transform using Style Dictionary
  4. Generate platform-specific code

This means one source of truth for colors across web, iOS, Android, and design tools.

Tools for Design Tokens

Chrome Extension extracting CSS variables

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:

  1. Visit any website
  2. Click the extension icon
  3. See all their CSS custom properties
  4. 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

  1. Extract CSS variables from 3-5 sites in your industry
  2. Analyze common patterns in naming and organization
  3. 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.

Get the Free Chrome Extension

Real-world color system examples

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:

  1. Audit your current CSS - How many hardcoded colors do you have?
  2. Create your variable structure - Use the three-tier naming system
  3. Implement basic theming - Add dark mode support
  4. Extract inspiration - Use our Chrome Extension to study great sites
  5. Build your palette - Try the Light/Dark Mode Generator for instant dark themes

Related Tools

Further Reading

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.