⚡ Phase 3 🔴 Intermediate ⏱️ 35 min ✨ 5 sections

CSS Variables & Theming

🎨 --custom-properties · :root · fallbacks · dark/light mode · JS integration
🏆 Lesson 11 of 12
Lesson progress0%
CSS custom properties (called CSS variables) are one of the most powerful features in modern CSS. They let you define reusable values once and use them everywhere — and unlike Sass variables, they're live in the browser: you can change them with JavaScript or media queries and every element that uses them updates instantly. This is how professional design systems and dark/light themes are built.

Defining & Using Custom Properties

Custom properties start with -- (two dashes) and are used with the var() function. They're case-sensitive and can hold any valid CSS value — colors, sizes, durations, even partial values.

Declaring and using CSS variables
CSS
/* Declare on :root — globally available */
:root {
  --color-primary: #2de8c0;
  --color-bg: #05091a;
  --color-text: #eef3ff;
  --font-size-base: 1rem;
  --spacing-md: 1.5rem;
  --radius: 12px;
  --transition: 0.2s ease;
}

/* Use with var() */
.btn {
  background: var(--color-primary);
  border-radius: var(--radius);
  padding: var(--spacing-md);
  transition: var(--transition);
}

/* Works anywhere a value is valid */
.card {
  border: 1px solid var(--color-primary);
  box-shadow: 0 4px 20px rgba(45, 232, 192, 0.2);
  font-size: var(--font-size-base);
}

/* Variables can reference other variables */
:root {
  --color-primary-rgb: 45, 232, 192;
  --color-primary-alpha: rgba(var(--color-primary-rgb), 0.15);
}
💡
Why not just use regular CSS values?
If your brand color is #2de8c0 and it appears in 50 places in your CSS, changing the brand requires 50 edits. With --color-primary, you change one line and every element updates. Variables also allow things regular values can't: runtime switching, component overrides, and JavaScript integration.
Variables in calc() — combining with math
CSS
:root {
  --space-1: 0.25rem;   /* 4px */
  --space-2: 0.5rem;    /* 8px */
  --space-4: 1rem;      /* 16px */
  --space-8: 2rem;      /* 32px */
}

.card {
  padding: var(--space-4);
  gap: var(--space-2);
  /* calc() works great with variables */
  width: calc(100% - var(--space-8));
}

/* Stagger animations with calc + --i */
.card { animation-delay: calc(var(--i) * 0.1s); }
/* Set --i from HTML: style="--i: 1" */

Scope: :root vs Element Scope

CSS variables cascade and inherit just like regular CSS. When you declare a variable on an element, it's only available to that element and its descendants — this is the key to component-level theming.

Global vs scoped variables
CSS
/* Global — available everywhere */
:root {
  --accent: #2de8c0;
  --bg: #05091a;
}

/* Scoped — override just for this component */
.card--danger {
  --accent: #f87171;    /* only inside .card--danger */
  --bg: rgba(248,113,113,.08);
}

.card--success {
  --accent: #4ade80;
  --bg: rgba(74,222,128,.08);
}

/* The .card styles are the same — only the vars change */
.card {
  background: var(--bg);
  border: 1px solid var(--accent);
  color: var(--accent);
}
:ROOT SCOPE (Global)
These cards share one CSS rule.
✅ Success card
❌ Danger card
⚠️ Warning card
ELEMENT SCOPE (Local Override)
One CSS rule — --accent changes per card.
.card { border: 1px solid var(--accent); }
.card-a { --accent: #2de8c0; }
.card-b { --accent: #f87171; }
.card-c { --accent: #fbbf24; }
Component theming pattern
This scoped-variable technique is how design systems like Tailwind and Material UI work under the hood. You write the component CSS once using variable names, then generate variants by overriding those variables on a wrapper class. One CSS rule, infinite color combinations.

Fallback Values & JavaScript Integration

The var() function accepts an optional fallback value — used if the variable isn't defined. And since CSS variables live in the DOM's computed style, JavaScript can read and write them at runtime, enabling dynamic theming without reloading the page.

Fallback values — the second argument to var()
CSS
/* var(--property, fallback) */
.btn {
  /* If --btn-bg isn't defined, use #2de8c0 */
  background: var(--btn-bg, #2de8c0);

  /* Fallback can be another variable */
  color: var(--btn-text, var(--color-text, #fff));
}

/* Nested fallbacks (chain of backups) */
.card {
  border-radius: var(--card-radius, var(--radius, 12px));
}
JavaScript: read and write CSS variables at runtime
JS
// READ a CSS variable
const value = getComputedStyle(document.documentElement)
  .getPropertyValue('--color-primary').trim();
// → "#2de8c0"

// WRITE a CSS variable (instantly updates all elements using it)
document.documentElement.style.setProperty('--color-primary', '#f97316');

// REMOVE (resets to :root value)
document.documentElement.style.removeProperty('--color-primary');

// Scoped to an element (not just :root)
const card = document.querySelector('.card');
card.style.setProperty('--accent', '#a78bfa');
🎮 Live JS Variable Demo
--demo-color
--demo-color: #2de8c0
--demo-radius
--demo-radius: 12px
Card A
Card B
Card C

↑ Use the controls to update CSS variables — all cards change at once

Dark / Light Mode Toggle

CSS variables make theme switching trivial. Define your entire color palette as variables on :root, then override those same variables under a [data-theme="light"] attribute (or body.light class). One JavaScript toggle, zero page reloads.

The complete dark/light mode pattern
CSS + JS
/* ─── DARK THEME (default) ─── */
:root {
  --bg: #05091a;
  --bg-card: #0a1230;
  --text: #eef3ff;
  --text-muted: #8ca8d8;
  --border: rgba(255,255,255,.08);
  --accent: #2de8c0;
}

/* ─── LIGHT THEME — override the same vars ─── */
[data-theme="light"] {
  --bg: #f8fafc;
  --bg-card: #ffffff;
  --text: #0f172a;
  --text-muted: #64748b;
  --border: #e2e8f0;
  --accent: #0d9488;
}

/* ─── USE the variables everywhere ─── */
body { background: var(--bg); color: var(--text); }
.card { background: var(--bg-card); border: 1px solid var(--border); }

/* ─── RESPECT system preference automatically ─── */
@media (prefers-color-scheme: light) {
  :root { /* apply light theme vars here */ }
}

// JavaScript — one liner! 
const toggle = () => {
  const current = document.documentElement.dataset.theme;
  document.documentElement.dataset.theme =
    current === 'light' ? 'dark' : 'light';
};
Theme Preview
CSS Variables Are Powerful
This entire component switches with one class change on a parent element. No JavaScript needed for the CSS — just a toggle on the data-theme attribute.
Zero JavaScript in CSS
The colors, backgrounds, borders — everything adapts purely through CSS variable overrides. The HTML and layout never change.
🌙
System preference + manual override
Pro pattern: default to @media (prefers-color-scheme: dark), then let users override with a toggle stored in localStorage. On page load, read localStorage and apply data-theme before rendering to prevent a flash of wrong theme.

Dynamic Theming Patterns

CSS variables unlock design patterns that were previously impossible without JavaScript frameworks. Here are the most useful ones for real projects.

Pattern 1: Alpha variants from a single RGB variable
CSS
/* Store color as RGB channels — then use rgba() for any opacity */
:root {
  --accent-rgb: 45, 232, 192;        /* just the numbers */
}

/* Now generate any opacity without repeating the color */
.card-hover  { background: rgba(var(--accent-rgb), 0.1); }
.card-border { border-color: rgba(var(--accent-rgb), 0.3); }
.card-glow   { box-shadow: 0 0 30px rgba(var(--accent-rgb), 0.4); }
Pattern 2: Responsive spacing scale
CSS
:root {
  --space-unit: 1rem;   /* base unit */
}

/* Tighten everything on mobile by changing ONE variable */
@media (max-width: 640px) {
  :root { --space-unit: 0.75rem; }
}

.section  { padding: calc(var(--space-unit) * 4); }
.card     { padding: calc(var(--space-unit) * 1.5); }
.btn      { padding: calc(var(--space-unit) * 0.5) calc(var(--space-unit) * 1.5); }
Pattern 3: User-customizable accent color (like Notion)
JS + CSS
// Let users pick their brand color — update the whole UI instantly
function setAccentColor(hex) {
  document.documentElement.style.setProperty('--accent', hex);
  localStorage.setItem('accent-color', hex);
}

// On page load, restore saved preference
const saved = localStorage.getItem('accent-color');
if (saved) setAccentColor(saved);
PatternUse caseKey technique
Dark/light themeSite-wide color switching[data-theme] attribute + var() overrides
Component variantsdanger/success/warning cardsScoped var() + single shared CSS rule
Responsive scaleSpacing adapts to viewportRedefine base var in @media
Alpha variantsMultiple opacities of one colorRGB store + rgba(var(), opacity)
User customizationUser-chosen accent colorJS setProperty() + localStorage
🧠 Knowledge Check
5 questions — master CSS custom properties.
1. What is the correct syntax to declare a CSS custom property called primary-color?
2. You declare --accent: teal on :root, but override --accent: red on a .card element. What color will a <p> inside .card see for var(--accent)?
3. What does var(--gap, 1rem) mean?
4. Which JavaScript method do you use to update a CSS variable at runtime?
5. Unlike Sass/LESS variables, what makes CSS custom properties unique?
🏆
Coding Challenge
Estimated time: 20–30 min
Build a Dark/Light Theme Switcher

Create a single-page layout with: (1) a :root block with at least 6 CSS variables (bg, card-bg, text, text-muted, border, accent), (2) a [data-theme="light"] override block that redefines all of them for light mode, (3) a nav with a toggle button that swaps the theme on click, (4) at least one card component, a heading, a paragraph, and a button — all using var() for all colors. Bonus: store the selected theme in localStorage and restore it on page load.
💡 Show hints
  • Put data-theme="dark" on <html> by default
  • Toggle: document.documentElement.dataset.theme = isLight ? 'dark' : 'light'
  • Add transition: background 0.3s ease, color 0.3s ease to body for smooth switching
  • Use localStorage.setItem('theme', value) to persist, and read on load with localStorage.getItem('theme')
  • The toggle icon: ☀️ when dark mode is active (click to go light), 🌙 when light
🎉
Lesson 11 Complete!
CSS Variables & Theming mastered! You can now build dynamic, maintainable design systems. One more lesson to go — Modern CSS & Best Practices.
← Course Home
Phase 3 · Advanced CSSLesson 11 of 12