Course topics

By WebNest Studio Engineering Team

CSS Tutorial

CSS Custom Properties (Variables)

CSS custom properties, commonly called CSS variables, let you define a reusable value once and reference it throughout a stylesheet, and even update it dynamically at runtime with JavaScript or media queries. Before custom properties existed, CSS had no native concept of a variable, forcing teams to rely on preprocessors like Sass purely for that feature.

A custom property is declared with a name prefixed by two dashes (e.g. --primary-color) and is read anywhere with the var() function. Because custom properties participate in the cascade and inheritance like any other CSS property, they can be scoped globally on :root or overridden locally within any selector.

Why it matters

A brand colour used in fifty places should be defined once. CSS custom properties make that possible without a build tool, and because they are live in the browser they can be changed at runtime, which is how most dark-mode switches work. Design systems are built on them.

Declaring and Using Custom Properties

Custom properties are typically declared on :root (a pseudo-class matching the <html> element) so they're available globally: :root { --primary-color: #0057d9; }. They're consumed anywhere with var(--primary-color), and var() accepts an optional second argument as a fallback value if the variable is undefined: var(--gap, 1rem).

Scoping and Overriding

Because custom properties inherit and follow the cascade, redeclaring a variable inside a more specific selector overrides it only within that selector's scope, without affecting the global value. This makes them ideal for theming — a .dark-theme class can redefine --background-color and --text-color, and every component using var() for those values updates automatically wherever that class is applied.

Custom Properties vs. Preprocessor Variables

Sass or Less variables are resolved once at compile time and produce static CSS — they cannot change based on runtime conditions. CSS custom properties are live: they can be read and changed by JavaScript (element.style.setProperty('--x', 'value')), and they respond to media queries and pseudo-class state changes, making them strictly more powerful for dynamic theming, even though they don't include preprocessor features like mixins or nesting.

Examples

Global theme variables consumed across multiple components

CSS
:root {
  --primary-color: #0057d9;
  --spacing-unit: 1rem;
  --border-radius: 8px;
}

.button {
  background: var(--primary-color);
  padding: var(--spacing-unit);
  border-radius: var(--border-radius);
}

.card {
  border: 1px solid var(--primary-color);
  padding: calc(var(--spacing-unit) * 2);
}
Output
Both the button and the card pull their color, spacing, and radius from the same shared variables, so changing --primary-color in one place updates every consumer at once.

A dark theme override using a scoped custom property

CSS
:root {
  --bg-color: #ffffff;
  --text-color: #111111;
}

.dark-theme {
  --bg-color: #111111;
  --text-color: #f5f5f5;
}

body {
  background: var(--bg-color);
  color: var(--text-color);
}
Output
Applying the .dark-theme class to a container (or the body) flips the background and text colors for everything inside it, since the redefined variables cascade down without touching the :root defaults elsewhere.

Common Mistakes

  • Forgetting the double-dash prefix on the variable name (writing primary-color instead of --primary-color), which silently fails since it is not recognized as a custom property.
  • Expecting a custom property defined inside a class to be available globally, without realizing its scope is limited to that selector and its descendants.
  • Not providing a fallback value in var(--name, fallback) for optional variables, risking an unstyled result if the variable is ever undefined.
  • Confusing CSS custom properties with Sass/Less variables and assuming both resolve at the same time — custom properties are live and can change at runtime.

Exercise

Define two custom properties on the root: a brand colour of #0f766e and a spacing value of 1rem. Use the brand colour for the heading text and the button background, and the spacing value for the padding of main. Give the button white text.

Expected result: a teal heading, a teal button with white text, and padding around the content.

CSS · starter code
/* The page contains: <main> with an <h1>, a <p> and a <button> */

:root {
  /* TODO: --brand and --space */
}

/* TODO: use the variables on h1, button and main */
Show hints
  • A custom property name starts with two hyphens: --brand: #0f766e;.
  • It is read with var(--brand).
Show solution
CSS · solution
/* The page contains: <main> with an <h1>, a <p> and a <button> */

:root {
  --brand: #0f766e;
  --space: 1rem;
}

h1 {
  color: var(--brand);
}

button {
  background: var(--brand);
  color: white;
}

main {
  padding: var(--space);
}

Quick quiz

1. How is a CSS custom property declared?

  • A. $brand: blue;
  • B. @brand: blue;
  • C. --brand: blue;
  • D. var brand = blue;
Show answer

C. The $ and @ forms belong to the Sass and Less preprocessors.

2. What does var(--gap, 8px) do?

  • A. Sets --gap to 8px
  • B. It is invalid
  • C. Adds 8px to --gap
  • D. Uses the value of --gap, or 8px if --gap is not defined
Show answer

D. The second argument is a fallback value.

3. Why are custom properties usually declared on :root?

  • A. They are inherited, so declaring them on the root makes them available to every element
  • B. It is the only place they are allowed
  • C. It makes them faster
  • D. It makes them constant
Show answer

A. Declaring one on a narrower selector scopes it to that element and its descendants.

Interview questions

What is the difference between CSS custom properties and Sass variables?

Show answer

A Sass variable is replaced by its value when the stylesheet is compiled, so it does not exist in the browser. A CSS custom property exists at runtime: it takes part in the cascade, is inherited, can be given a different value inside a selector or a media query, and can be read and changed with JavaScript. Custom properties therefore support live theming, which Sass variables cannot.

How would you implement a dark theme with custom properties?

Show answer

Define the colours as custom properties on :root, such as --bg and --text, and use only those variables in the component styles. Then redefine the same properties for the dark theme, either inside @media (prefers-color-scheme: dark) to follow the system setting, or under a selector such as [data-theme="dark"] that a toggle button sets. No component rule has to change.

Key Points to Remember

  • Custom properties are declared as --name: value and read with var(--name), often defined globally on :root.
  • var() supports a fallback second argument: var(--name, fallback-value).
  • Custom properties inherit and follow the cascade, so they can be overridden locally for scoped theming.
  • Unlike preprocessor variables, CSS custom properties are live and can be changed at runtime via JavaScript or media queries.

Practice in the playground

Write and run your own code for this topic, or start from the exercise or an example. Shows a live preview of your HTML, CSS and JavaScript. Your code for each topic is kept in this browser.

Run the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

WebNest Studio

Written by

WebNest Studio Engineering Team

Practical lessons from developers working with Java, Python, React, databases and production systems.

Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering