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.