Episode 1 — Fundamentals / 1.6 — CSS Core Fundamentals

1.6.g — CSS Variables (Custom Properties)

In one sentence: CSS custom properties (--name) let you define reusable, inheritable, runtime-updatable values — they are the foundation of theming, design tokens, and dynamic styles without preprocessors.

Navigation: ← 1.6.f — Modern CSS Functions · 1.6.h — Design Systems →


1. Syntax

Defining a variable

:root {
  --color-primary: #2563eb;
  --spacing-md: 1rem;
  --font-body: system-ui, sans-serif;
}
  • -- prefix is required (double hyphen).
  • Defined on any selector — :root makes them globally available via inheritance.
  • Values are inherited down the DOM tree like color or font-family.

Using a variable

.button {
  background-color: var(--color-primary);
  padding: var(--spacing-md);
  font-family: var(--font-body);
}

Fallback values

.card {
  color: var(--color-text, #333);
  /* If --color-text is not defined, use #333 */
}

Fallbacks can be nested: var(--a, var(--b, red)).


2. Why :root?

:root targets the <html> element with a specificity of a pseudo-class (0,0,1,0). Defining variables there makes them available to every element via inheritance.

:root {
  --radius: 0.5rem;
}

/* Every element can use --radius */
.card   { border-radius: var(--radius); }
.button { border-radius: var(--radius); }
.input  { border-radius: var(--radius); }

3. Scoping — local overrides

Custom properties follow inheritance, so you can override them at any level:

:root {
  --color-primary: #2563eb;   /* blue */
}

.dark-theme {
  --color-primary: #60a5fa;   /* lighter blue for dark backgrounds */
}

.danger-zone {
  --color-primary: #dc2626;   /* red in this section */
}

Any descendant of .dark-theme that uses var(--color-primary) gets the lighter blue — no extra selectors needed.


4. Theming with custom properties

Light / dark mode

:root {
  --bg: #ffffff;
  --text: #1a1a2e;
  --surface: #f1f5f9;
}

[data-theme="dark"] {
  --bg: #0f172a;
  --text: #e2e8f0;
  --surface: #1e293b;
}

body {
  background-color: var(--bg);
  color: var(--text);
}

Switch themes by toggling a data-theme attribute — all styles update automatically through variable inheritance.

Respecting user preference

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0f172a;
    --text: #e2e8f0;
  }
}

5. Dynamic updates with JavaScript

Custom properties are live — changing them at runtime updates all dependent styles immediately:

document.documentElement.style.setProperty('--color-primary', '#10b981');

This is far more efficient than toggling classes for every affected element — the browser recomputes styles from the single variable change.


6. Custom properties vs preprocessor variables

CSS Custom PropertiesSass/Less Variables
RuntimeLive — computed at render timeCompiled away at build time
InheritanceYes — flows down the DOMNo — flat scope
ThemingYes — override per contextRequires recompilation
JS accessYes — getComputedStyle / setPropertyNo — must rebuild CSS
FallbacksBuilt-in var(--x, fallback)Preprocessor conditionals

CSS custom properties and preprocessor variables are complementary — use preprocessor variables for build-time constants (file paths, mixins) and custom properties for runtime-dynamic values (themes, user preferences).


7. Patterns and tips

Spacing scale

:root {
  --space-xs: 0.25rem;
  --space-sm: 0.5rem;
  --space-md: 1rem;
  --space-lg: 2rem;
  --space-xl: 4rem;
}

Component-scoped variables

.card {
  --card-padding: 1.5rem;
  --card-radius: 0.75rem;

  padding: var(--card-padding);
  border-radius: var(--card-radius);
}

.card.compact {
  --card-padding: 0.75rem;
  --card-radius: 0.5rem;
}

The component owns its knobs — consumers override the variables, not the internals.

Computed variables with calc()

:root {
  --base-size: 1rem;
  --scale: 1.25;
}

h3 { font-size: calc(var(--base-size) * var(--scale)); }
h2 { font-size: calc(var(--base-size) * var(--scale) * var(--scale)); }
h1 { font-size: calc(var(--base-size) * var(--scale) * var(--scale) * var(--scale)); }

8. Key takeaways

  1. --custom-property defined, var(--custom-property) consumed.
  2. Defined on :root = globally available; scoped to any selector for local overrides.
  3. Custom properties inherit — override in a parent, all descendants update.
  4. Theming (dark mode, brand variants) becomes trivial with variable overrides.
  5. JS can update custom properties at runtime — no rebuild needed.
  6. Use var(--x, fallback) for defensive coding.

Explain-It Challenge

Explain without notes:

  1. Why defining variables on :root makes them globally available.
  2. How to implement dark mode using only CSS custom properties and a data-theme attribute.
  3. One advantage CSS custom properties have over Sass variables at runtime.

Navigation: ← 1.6.f — Modern CSS Functions · 1.6.h — Design Systems →