Himanshu SharmaSoftware engineer
Let’s talk

CSS · Lesson 28 of 29

CSS variables

Store values in CSS custom properties with var(), keep a design consistent, scope variables to components, build a dark theme and set them from JavaScript.

13 minPolishUpdated 18 October 2026

By the end of this lessonDeclare custom properties, reuse them with var and fallbacks, switch themes by changing a few values, and know how inheritance affects them.

Name a value once

Look at any stylesheet and the same few values appear again and again: the brand colour, the spacing unit, the corner radius. Changing the brand colour means finding and editing every copy. CSS custom properties, often called CSS variables, solve this: store a value once under a name, use it everywhere, and change it in one place.

Declare and use

A custom property is any name starting with two dashes. You declare it like a normal property and read it with var():

html
<style>
  :root {
    --brand: #4f46e5;
    --radius: 10px;
    --space: 1rem;
  }
  .btn {
    background: var(--brand);
    color: white;
    padding: var(--space) calc(var(--space) * 2);
    border: 0;
    border-radius: var(--radius);
  }
  .card {
    border: 2px solid var(--brand);
    border-radius: var(--radius);
    padding: var(--space);
    margin-bottom: var(--space);
  }
</style>
<div class="card">A card using the brand colour.</div>
<button class="btn">A button using it too</button>

Change --brand in the first rule and both elements update. Names are case-sensitive and entirely up to you: --color-primary, --text, --space-4.

:root and scope

:root selects the top element (html), so variables defined there are global: every element inherits them. You can also declare variables on any element, which makes them local to that element and its descendants:

html
<style>
  :root { --accent: #2563eb; }
  .warning { --accent: #dc2626; }          /* overrides inside .warning */
  .note { border-left: 6px solid var(--accent); padding: 8px 12px; margin: 8px 0; background: #f8fafc; }
</style>
<div class="note">Normal note (blue)</div>
<div class="warning"><div class="note">Warning note (red): same CSS, new value</div></div>

The .note rule never changed. It just receives a different --accent depending on where it sits. This is how components become themeable.

Fallbacks

var() takes an optional fallback after a comma, used when the variable is undefined:

css
color: var(--heading-color, #111827);
padding: var(--pad, 1rem 1.5rem);

Theming and dark mode

Because every rule reads from variables, a theme is just a different set of values:

html
<style>
  :root { --bg: #ffffff; --text: #111827; --card: #f1f5f9; }
  @media (prefers-color-scheme: dark) {
    :root { --bg: #0f172a; --text: #e2e8f0; --card: #1e293b; }
  }
  body { background: var(--bg); color: var(--text); font-family: system-ui, sans-serif; padding: 16px; }
  .card { background: var(--card); padding: 16px; border-radius: 10px; }
</style>
<div class="card">This follows your system's light or dark setting.</div>

For a manual toggle, put the dark values under a class or attribute, [data-theme="dark"] { … }, and switch that attribute with a small script. Always keep the contrast accessible in both themes.

Variables with calc() and other functions

css
:root { --space: 8px; }
.stack > * + * { margin-top: calc(var(--space) * 3); }
.box { width: min(100% - 2rem, 60rem); }

You can store the number and do the maths where it is used. Storing parts of colours also works: --hue: 250; background: hsl(var(--hue) 80% 50%); lets you derive a whole palette from a single number.

Using variables in components

A clean pattern is to expose a few local variables that callers can override:

css
.btn {
  --btn-bg: var(--brand);
  background: var(--btn-bg);
}
.btn.danger { --btn-bg: #dc2626; }

Changing variables with JavaScript

Variables exist in the browser, so scripts can change them live:

js
document.documentElement.style.setProperty('--brand', '#059669');
const current = getComputedStyle(document.documentElement).getPropertyValue('--brand');

That is something Sass-style compile-time variables cannot do.

Rules and limits

  • Names are case-sensitive: --Brand and --brand differ.
  • A variable is only usable as a value, never as a property name or inside a selector or media query condition.
  • Invalid values are not detected until var() is used; if a variable is invalid at that point, the property falls back to its initial value.
  • Variables inherit, so a value set on a parent flows to children.
  • Browser support is universal in modern browsers.

Organising variables

Group them by purpose and keep names meaningful:

css
:root {
  /* colour */
  --color-text: #111827;
  --color-brand: #4f46e5;
  /* spacing scale */
  --space-1: 0.25rem; --space-2: 0.5rem; --space-3: 1rem; --space-4: 2rem;
  /* shape and depth */
  --radius: 10px;
  --shadow-1: 0 1px 3px rgb(0 0 0 / .15);
}

Name variables for their role (--color-text) more than their appearance (--dark-grey), so the theme can change without the names lying.

Common mistakes

  • Forgetting the two dashes, or writing --brand where you need var(--brand).
  • Declaring on the wrong element, so the variable is not inherited where you use it.
  • Names tied to looks (--blue) that stop making sense after a rebrand.
  • No fallback for variables set by scripts.
  • Dark themes with poor contrast.

Exercise

Define colour and spacing variables on :root, build a card and a button from them, and create a .compact class that halves the spacing for anything inside it without editing the card or button rules.

Show one possible answer
html
<style>
  :root { --brand: #7c3aed; --space: 1rem; --radius: 10px; }
  .compact { --space: 0.5rem; }
  .card { padding: var(--space); border: 2px solid var(--brand); border-radius: var(--radius); margin-bottom: var(--space); }
  .btn { padding: var(--space) calc(var(--space) * 2); background: var(--brand); color: white; border: 0; border-radius: var(--radius); }
</style>
<div class="card">Normal card <button class="btn">Go</button></div>
<div class="compact"><div class="card">Compact card <button class="btn">Go</button></div></div>

Key points

  • A custom property starts with two dashes, is declared like any property, and is read with var().
  • Declaring variables on :root makes them available everywhere, and declaring them on a component limits their scope.
  • Variables inherit and can be changed by media queries, so a dark theme means redefining a few values.
  • var() accepts a fallback, used when the variable is not defined.
  • Unlike preprocessor variables, CSS variables live in the browser and can be changed at runtime.

Check yourself

1. How do you read a custom property named --brand?
2. Where do you usually declare global variables?
3. How can you make a dark theme with variables?
4. What does var(--gap, 1rem) mean?

Free tools for this lesson