Skip to content
Innovialabs

CSS Variables (Custom Properties): A Practical Guide

Basics4 min

In short

A CSS variable is declared as --name: value and read with var(--name); it inherits down the tree and can change in media queries and from JavaScript right in the browser, which is what sets it apart from Sass variables.

The short answer

CSS variables (officially custom properties) let you define a value once and reuse it in many places. The declaration starts with two hyphens, and you read it with the var() function:

:root {
  --color-primary: #7c3aed;
  --space-m: 1rem;
}

.button {
  background: var(--color-primary);
  padding: var(--space-m);
}

Change the value in one place and every button updates. Unlike preprocessor variables, CSS variables live in the browser: you can override them for individual blocks, in media queries and from JavaScript.

Syntax essentials

  • The name starts with -- and is case-sensitive: --Color and --color are different variables.
  • The value can be almost anything: a color, a size, a font stack, part of a value.
  • A variable is a property, so it is declared inside a selector, not at the top level of the file.
  • You cannot use a variable in a media query condition (@media (min-width: var(--bp)) will not work) or as a property name.

Scope and inheritance

Custom properties follow the normal cascade and are inherited by descendants. A variable declared on :root is available everywhere. One declared on a component is available only inside it.

:root { --accent: #7c3aed; }

.card--warning { --accent: #d97706; }

.card .title { color: var(--accent); }

All card titles are violet, but the warning card's are orange. The component reads the same variable and is "configured" from outside. This is the basis for flexible components without a dozen modifiers.

You can override values in media queries too:

:root { --gutter: 1rem; }
@media (min-width: 768px) {
  :root { --gutter: 2rem; }
}

Fallback values

The second argument of var() is a fallback, used when the variable is not defined:

.box { padding: var(--box-padding, 1rem); }

One catch: a fallback does not protect against an invalid value. If --size: red and you write width: var(--size), the declaration becomes invalid and the property gets its default value, not the fallback. The browser will not flag this as a syntax error, so check such bugs in DevTools.

Changing variables from JavaScript

This is the main advantage over Sass: values change on the fly.

const root = document.documentElement;

// write
root.style.setProperty('--accent', '#5b21b6');

// read
const accent = getComputedStyle(root).getPropertyValue('--accent').trim();

Typical use cases: cursor position for effects, scroll progress, a user-picked color, theme switching. JS changes one variable, and all display logic stays in CSS.

Design tokens and themes

Design tokens are the named decisions of a design system: colors, spacing, radii, shadows. CSS variables are a convenient way to bring them into code. A good practice is two layers:

  1. Base tokens — the palette: --violet-600, --gray-100.
  2. Semantic tokens — roles: --color-bg, --color-text, --color-accent.

Components use only semantic tokens. A dark theme then becomes an override of a few roles:

:root {
  --color-bg: #ffffff;
  --color-text: #1a1033;
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-bg: #120b24;
    --color-text: #ede9fe;
  }
}

[data-theme="dark"] {
  --color-bg: #120b24;
  --color-text: #ede9fe;
}

How they differ from Sass variables

Sass variablesCSS variables
When resolvedAt build timeIn the browser
Present in the output CSSNo, replaced with valuesYes
Cascade and inheritanceNoYes
Changeable from JS and media queriesNoYes
Usable in @media conditionsYesNo

They are not rivals. Sass is handy for build-time calculations and mixins; CSS variables are for anything that should change at runtime.

Common mistakes

  • Creating a variable for every single value — you end up with hundreds and nobody remembers what each one is for.
  • Naming by appearance (--purple) where a role is needed (--color-accent) — after a rebrand, the name lies.
  • Ignoring whitespace: in some browsers getPropertyValue returns the value with a leading space — add trim().

FAQ

Yes, all modern browsers have supported them for a long time. Issues appear only in very old browsers that rarely need to be considered today.

We can help with this

If you'd rather get the result than figure it all out yourself — this is what we do every day.

Discuss a project

Read next

Have an idea or a task? Tell us about it

We'll reply within one business day and propose a solution. The consultation is free.