CSS Custom Properties

CSS Variables

Custom properties are values you define once and reuse anywhere. Unlike preprocessor variables, they live in the browser and can change at runtime.

intermediate13 min readUpdated Sep 15, 2026
theme.css
css
/* theme.css */
:root {
  --color-bg: #ffffff;
  --color-text: #0f172a;
  --space: 1rem;
}

[data-theme="dark"] {
  --color-bg: #0b0d0c;
  --color-text: #e5e7eb;
}

body {
  background: var(--color-bg);
  color: var(--color-text);
  padding: var(--space);
}
Syntax
--name: value
Read with
var(--name)
Scope
Cascades and inherits
Runtime
Changeable with JS
Fallback
var(--x, red)
Typing
@property

Why it matters

Why custom properties matter

One source of truth

Define a colour, space or radius once and reference it everywhere, so changes happen in a single place.

Change at runtime

Custom properties live in the DOM, so JavaScript, media queries and data attributes can change them live.

Theming made easy

Swap a set of variables on a root or component and the whole subtree re-themes instantly.

The big picture

The three ideas behind custom properties

Declare a value, read it with var(), and let the cascade decide which declaration applies.

Declare

Define

A property name starting with -- holds any value you want to reuse.

Use

Reference

var() reads the value, with an optional fallback if it is not defined.

Cascade

Resolve

The nearest declaration wins, which is what makes scoping and theming work.

CSS variables at a glance

The core of custom properties

Declaration

--name: value, usually on :root for global tokens.

var()

Read a custom property, optionally with a fallback value.

Inheritance

Custom properties inherit, so children see their parent's values.

Theming

Override variables on a selector to change a whole subtree.

Dynamic values

Set them from JavaScript with style.setProperty.

@property

Register a property with a type for animation and validation.

A short history

From preprocessors to native CSS

  1. 2007

    Sass variables

    Preprocessors popularise reusable values, resolved at build time.

    07
  2. 2012

    CSS variables proposed

    A native, runtime equivalent is designed for CSS.

    12
  3. 2016

    Browser support

    Custom properties ship in all major browsers.

    16
  4. 2020

    Design tokens

    Design systems adopt custom properties as the token layer.

    20
  5. 2024

    Typed properties

    @property enables smooth animation and validation of custom properties.

    24

The complete guide

CSS Variables: Everything you need to know

What are CSS variables?

CSS variables, properly called custom properties, are values you define once and reference anywhere. They look like ordinary properties but start with --, and you read them with the var() function.

/* tokens.css */
:root {
  --color-brand: #2563eb;
  --radius: 0.5rem;
  --space-4: 1rem;
}

.button {
  background: var(--color-brand);
  border-radius: var(--radius);
  padding: var(--space-4) calc(var(--space-4) * 1.5);
}

The key difference from a preprocessor variable is that custom properties are part of the browser’s cascade. They inherit, can be scoped, and can change at runtime. That makes them the natural layer for theming and design tokens.

Declaring and using

Declare a custom property with a name that starts with two dashes and any valid value.

/* declare.css */
:root {
  --gap: 1.5rem;
  --shadow: 0 1px 2px rgb(0 0 0 / 0.08);
  --font-sans: "Inter", system-ui, sans-serif;
}

.card {
  gap: var(--gap);
  box-shadow: var(--shadow);
  font-family: var(--font-sans);
}

Values can be almost anything: colours, lengths, shadows, font stacks, gradients or even fragments of a value. Because they are substituted before the property is parsed, they can be combined with calc().

The cascade and inheritance

Custom properties follow normal CSS rules. A declaration on an element is inherited by its descendants, and the nearest declaration wins.

/* scope.css */
:root {
  --accent: #2563eb;
}

.sidebar {
  --accent: #f97316;
}

.sidebar a {
  color: var(--accent); /* orange inside the sidebar */
}

This is what makes scoping powerful: you can re-theme a component or a section without touching the global tokens. It is also why defining tokens on :root makes them available everywhere.

Fallbacks

var() accepts a fallback as a second argument, used when the property is missing or invalid.

/* fallback.css */
.button {
  color: var(--button-color, #ffffff);
  background: var(--button-bg, var(--color-brand, #2563eb));
}

Fallbacks are useful for reusable components that should work even when tokens are not defined, and they can be nested. Use them sparingly, though; if a token should always exist, define it clearly rather than hiding the problem.

Theming

Theming is the flagship use case. Swap a set of variables and an entire subtree updates.

/* theme.css */
:root {
  --bg: #ffffff;
  --surface: #f8fafc;
  --text: #0f172a;
  --border: #e2e8f0;
}

[data-theme="dark"] {
  --bg: #0b0d0c;
  --surface: #14161a;
  --text: #e5e7eb;
  --border: #26292e;
}

body {
  background: var(--bg);
  color: var(--text);
}

.card {
  background: var(--surface);
  border: 1px solid var(--border);
}

Toggle a data-theme attribute on the root element and the whole page re-themes instantly, with no duplicated rules. You can also scope a theme to a component, so a dark card can sit on a light page.

Dynamic values with JavaScript

Because custom properties live in the DOM, you can read and write them from JavaScript.

// progress.js
const bar = document.querySelector(".progress");

function setProgress(percent) {
  bar.style.setProperty("--progress", `${percent}%`);
}

setProgress(72);
/* progress.css */
.progress__fill {
  width: var(--progress, 0%);
}

This is how progress bars, drag interactions, cursor-following effects and theme toggles are commonly built. The value stays in CSS and JavaScript only updates a number.

Typed properties with @property

By default, custom properties are untyped strings, so the browser cannot interpolate them. The @property at-rule registers a type, an initial value and an inheritance behaviour.

/* property.css */
@property --angle {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

.spinner {
  background: conic-gradient(from var(--angle), #2563eb, transparent);
  animation: rotate 1s linear infinite;
}

@keyframes rotate {
  to { --angle: 360deg; }
}

With a registered type, the property becomes animatable and the browser validates the value, which prevents a whole class of bugs.

Best practices

  • Define global tokens on :root and use them everywhere.
  • Name tokens by purpose (--color-text) not appearance (--gray-900).
  • Override variables for themes and component variants instead of duplicating rules.
  • Provide fallbacks for reusable components that may run without tokens.
  • Update variables from JavaScript rather than writing inline styles everywhere.
  • Register animatable properties with @property.
  • Keep the token set small and consistent.

Common mistakes

  • Expecting custom properties to behave like Sass variables at build time.
  • Using vague names that make the token set hard to maintain.
  • Defining every value as a variable and losing readability.
  • Forgetting that values are untyped unless registered with @property.
  • Overriding tokens globally when only a subtree should change.
  • Storing secrets or user data in CSS variables, which are visible in the DOM.

Where to go next

Custom properties are the connective tissue of a maintainable stylesheet. Apply them to layout with Responsive Design, animate typed ones with Animations & Transitions, and understand the cascade behind them in the CSS guide. For build-time variables that complement them, see Sass.

Theming

Override a small set of variables to re-theme a whole subtree. Duplicating rules for each theme is error-prone.

Prefer
:root {
  --bg: #fff;
  --fg: #0f172a;
}

[data-theme="dark"] {
  --bg: #0b0d0c;
  --fg: #e5e7eb;
}

.card {
  background: var(--bg);
  color: var(--fg);
}
Avoid
.card {
  background: #fff;
  color: #0f172a;
}

[data-theme="dark"] .card {
  background: #0b0d0c;
  color: #e5e7eb;
}

Values that change at runtime

Custom properties are live in the DOM, so JavaScript can update them without rewriting stylesheets.

Prefer
// update a theme token
document.documentElement.style
  .setProperty("--progress", "72%");
Avoid
/* a preprocessor variable is
   fixed at build time and
   cannot change per user */

FAQ

Frequently asked questions

Keep learning

Related topics from the roadmap.

$ start learning

Ready to start learning CSS Variables?

Our interactive tutorial walks you through CSS Variables step by step — with quizzes and real code you can run in the browser.