🌐

CSS Variables Explained: Build Themes and Dark Mode

HTML/CSS3 min readUpdated October 2026

Learn how CSS custom properties work, how scoping and fallbacks behave, and how to build a dark mode toggle with a few lines of CSS and JavaScript.

Open a stylesheet from a real project and you'll often find the same color written forty times. When the brand color changes, you're on a find-and-replace hunt, hoping you didn't miss one.

CSS custom properties, usually called CSS variables, fix that. You define a value once, use it everywhere, and change it in one place.

Define and use a variable

A variable name starts with two dashes. You read it back with var().

<style>
  :root {
    --brand: #16a34a;
    --radius: 10px;
    --space: 16px;
  }

  .button {
    background: var(--brand);
    border-radius: var(--radius);
    padding: var(--space);
    color: white;
    border: none;
    font-size: 16px;
  }
  .card {
    border: 2px solid var(--brand);
    border-radius: var(--radius);
    padding: var(--space);
    margin-bottom: var(--space);
    font-family: sans-serif;
  }
</style>

<div class="card">A card using the brand color.</div>
<button class="button">A button using it too</button>

:root is the top of the document (the same as <html>), so anything defined there is available everywhere. Change --brand to #2563eb and both the card and button turn blue.

Fallback values

var() accepts a second argument, used if the variable isn't defined.

.note {
  color: var(--note-color, #444);
}

If --note-color doesn't exist, the text is #444. This is handy for components that should work with or without a theme.

Variables follow the cascade

Unlike variables in Sass, which are replaced at build time, CSS variables are live. They inherit down the tree, and you can override them for any part of the page.

<style>
  :root { --bg: #f3f4f6; --text: #111827; }

  .panel {
    background: var(--bg);
    color: var(--text);
    padding: 16px;
    margin-bottom: 12px;
    border-radius: 8px;
    font-family: sans-serif;
  }

  .panel.inverted {
    --bg: #111827;
    --text: #f9fafb;
  }
</style>

<div class="panel">Normal panel</div>
<div class="panel inverted">Same CSS, different variables</div>

The .panel rule is identical for both. Only the variables changed, and everything inside the inverted panel inherits the new values. This is how you build components that adapt to their surroundings.

Dark mode in a few lines

Define your colors as variables, then override them when the user prefers dark.

<style>
  :root {
    --bg: #ffffff;
    --text: #1f2937;
    --card: #f3f4f6;
  }
  @media (prefers-color-scheme: dark) {
    :root {
      --bg: #0f172a;
      --text: #e2e8f0;
      --card: #1e293b;
    }
  }

  body { background: var(--bg); color: var(--text); font-family: sans-serif; padding: 16px; }
  .card { background: var(--card); padding: 16px; border-radius: 8px; }
</style>

<h2>Hello</h2>
<div class="card">This page follows your system's light or dark setting.</div>

Change your operating system between light and dark and the page switches. You didn't touch a single rule below the variables.

A manual toggle with JavaScript

Many sites also let the visitor choose. The usual approach is a data-theme attribute on the <html> element.

<style>
  :root { --bg: #ffffff; --text: #1f2937; }
  :root[data-theme="dark"] { --bg: #0f172a; --text: #e2e8f0; }

  body {
    background: var(--bg);
    color: var(--text);
    font-family: sans-serif;
    padding: 16px;
    transition: background 0.3s, color 0.3s;
  }
</style>

<h2>Theme toggle</h2>
<button id="toggle">Switch theme</button>

<script>
  const root = document.documentElement;
  document.querySelector("#toggle").addEventListener("click", () => {
    const next = root.dataset.theme === "dark" ? "light" : "dark";
    root.dataset.theme = next;
  });
</script>

On a real site, save the choice (for example in localStorage) and apply it before the page paints, so it doesn't flash the wrong theme.

Reading and setting variables in JavaScript

Since they're live, JavaScript can change them at any time. That's perfect for sliders, color pickers, and user settings.

<style>
  :root { --size: 24px; }
  p { font-size: var(--size); font-family: sans-serif; }
</style>

<input id="range" type="range" min="12" max="48" value="24">
<p>Resize this text</p>

<script>
  document.querySelector("#range").addEventListener("input", (event) => {
    document.documentElement.style.setProperty("--size", event.target.value + "px");
  });
</script>

To read the current value, use getComputedStyle(element).getPropertyValue("--size").

Use variables with calc()

Variables work inside calc(), so you can build a whole spacing scale from one base.

:root {
  --space: 8px;
}
.stack > * + * { margin-top: calc(var(--space) * 2); }
.section       { padding: calc(var(--space) * 4); }

Change --space once and the whole rhythm of the page shifts.

What to put in variables

Good candidates are things that repeat or might change: colors, font sizes, spacing, border radius, shadows, and transition durations. Name them by purpose (--color-text, --color-surface) rather than by value (--dark-gray), so a theme change doesn't make the names lie.

Don't turn every number into a variable. A one-off margin-top: 3px doesn't need one.

Gotchas

  • Variables can't be used in media query conditions. @media (min-width: var(--bp)) doesn't work. The value there is fixed.
  • A typo gives you nothing, not an error. If var(--brnad) doesn't exist, the property becomes invalid and falls back to its inherited or initial value. Check the spelling.
  • Names are case-sensitive. --Brand and --brand are different.
  • Don't forget the fallback when a variable might be missing.

Key takeaways

  • Define with --name, use with var(--name), and put global ones on :root.
  • Variables inherit, so you can override them on any element to change a whole section.
  • Dark mode is a few overridden variables, either with prefers-color-scheme or a data-theme attribute.
  • JavaScript can change them live with setProperty.

Try it yourself

Reading helps, but typing the code is what makes it stick. Open the editor, paste any example from this article, and change it until it breaks.

Open the HTML/CSS editor

Related Articles