Custom properties (CSS variables)

CSS · Practical · 12 min

Variables make design tokens, theming and dark mode a matter of changing a few values.

A custom property is any property whose name starts with --. You define it like any declaration and read it with var():

:root { --brand: #4f46e5; }
.button { background: var(--brand); }

They're not preprocessor variables: they live in the browser, take part in the cascade and inherit. Defining --brand on :root makes it available everywhere; redefining it on .danger-zone changes it for that subtree only. Change one with JavaScript (el.style.setProperty("--x", "10px")) and everything using it updates live.

var(--gap, 1rem) supplies a fallback used when the variable isn't defined. Fallbacks can nest: var(--a, var(--b, 4px)).

Values are stored as token streams and only checked when used. If --size: red is used in width: var(--size), the declaration becomes invalid at computed-value time and the property falls back to its inherited or initial value, not to an earlier declaration in your stylesheet.

Variables work well as design tokens: colours, spacing scale, radii, fonts. Theming becomes overriding tokens:

  • @media (prefers-color-scheme: dark) { :root { --bg: #111; --fg: #eee; } },
  • or a [data-theme="dark"] attribute toggled by a script.

Variables can't be used in selectors or media query conditions. To animate one, or make it type-checked, register it with @property, covered with animations.

Example

<!-- index.html -->
<style>
  :root {
    --bg: #ffffff; --fg: #111827; --brand: #4f46e5;
    --space: 1rem; --radius: 8px;
  }
  @media (prefers-color-scheme: dark) {
    :root { --bg: #0b1020; --fg: #e5e7eb; }
  }
  body { background: var(--bg); color: var(--fg); }

  .button {
    background: var(--btn-bg, var(--brand));   /* local override, then token */
    padding: calc(var(--space) / 2) var(--space);
    border-radius: var(--radius);
    color: white;
  }
  .danger { --btn-bg: crimson; }   /* only this subtree changes */
</style>
<button class="button">Save</button>
<div class="danger"><button class="button">Delete</button></div>

Remember

  • --name defines, var(--name, fallback) reads
  • Custom properties cascade and inherit, so they can be scoped
  • Theming = redefining tokens under a media query or attribute
  • Invalid values at use time fall back to inherited/initial, not earlier rules

Watch out for

  • Expecting var() to work inside a media query condition like @media (min-width: var(--bp)).

Check yourself

  1. --pad is set to 8px on :root and 20px on .card. What padding does a .card .title with padding: var(--pad) get?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: developer.mozilla.org/en-US/docs/Web/CSS/Guides/Cascading_variables/Using_custom_properties