Skip to main content
Syllabus
On this page

CSS Variables (Custom Properties)

Badar KhalilUpdated September 30, 2026 8 min read

CSS Variables (Custom Properties): Complete Guide

Focus keyword: CSS variables. CSS variables, officially called custom properties, let you store a value once and reuse it everywhere. They follow the cascade and inheritance, they can change at runtime, and they are the foundation of theming, design tokens and dark mode. In this lesson you will learn the syntax, scope, fallbacks, JavaScript access, the @property rule and the most common mistakes. Every code block is editable, so change the values and watch the preview.

CSS custom properties, var() function, CSS variables vs Sass variables, CSS variable fallback, :root variables, design tokens, CSS theming, dark mode variables, @property rule, registered custom properties, animate CSS variables, setProperty JavaScript, getComputedStyle, CSS variables scope, component API with custom properties, invalid at computed-value time, container style queries, CSS variables in calc.

1. What Are CSS Variables?

A custom property is a property whose name you choose. The name always starts with two dashes, for example --brand-color. You read its value with the var() function. Unlike Sass or Less variables, which are replaced at build time, CSS variables live in the browser. They can be changed with media queries, classes, attributes, JavaScript and even animations.

  • Declare: --brand-color: #0b5fff;
  • Use: color: var(--brand-color);
  • Names are case sensitive: --Brand and --brand are different variables.
  • Values can be colors, lengths, numbers, strings, URLs, lists or even whole chunks of CSS such as 0 4px 12px rgba(0,0,0,.2).

2. Syntax and the :root Selector

Global variables are usually declared on :root, which matches the html element but has higher specificity than the html type selector. Everything in the page inherits them. Good habits: group variables by purpose (colors, spacing, typography, radius, shadow) and give them meaningful names such as --color-text instead of --dark-gray, because the meaning stays true when the value changes.

3. The var() Function and Fallback Values

var(--name, fallback) uses the fallback when the variable is not defined. The fallback can contain commas, for example var(--font, Inter, system-ui, sans-serif), and can be another variable: var(--accent, var(--brand, blue)). A fallback is not used when the variable exists but holds an invalid value for that property, see section 9.

4. Scope and Inheritance

Custom properties inherit like color or font-family. A variable declared on an element is available to that element and all of its descendants. You can override a variable on any selector, and only that part of the page changes. This is what makes local theming possible: set --card-bg on a single card, a section or a component wrapper.

5. Theming with Variables

Define your colors as variables and redefine them for a theme. For example, [data-theme='dark'] { --bg: #111; --text: #eee; } swaps the whole palette without touching any component rule. The same pattern works for brand themes, seasonal themes and high contrast modes. You will connect this idea with prefers-color-scheme in the dark mode lesson.

6. Variables and calc()

Variables combine very well with math functions. Store a base unit and build a scale: --space: 8px; padding: calc(var(--space) * 2);. Keep numbers unitless when you want to multiply them by different units, for example --cols: 3 and width: calc(100% / var(--cols)).

7. Reading and Writing Variables with JavaScript

  • Set on the root: document.documentElement.style.setProperty('--hue', 200).
  • Set on one element: el.style.setProperty('--progress', '60%').
  • Read the computed value: getComputedStyle(el).getPropertyValue('--hue').
  • Remove an inline value: el.style.removeProperty('--hue').

Changing a variable updates everything that uses it, without re-writing style rules. This is perfect for sliders, pointer-following effects, scroll progress and live theme pickers.

8. The @property Rule (Registered Custom Properties)

By default a custom property is just text, so the browser cannot animate it. With @property you register a type: @property --angle { syntax: '<angle>'; inherits: false; initial-value: 0deg; }. Now the browser understands the value, validates it, and can transition or animate it. This unlocks animated gradients, number counters and smooth color changes. The three descriptors are syntax (the type), inherits (true or false) and initial-value (required unless the syntax is universal). Check browser support and keep a static fallback.

9. Invalid at Computed-Value Time

A custom property accepts almost any value, so the browser cannot know if it is valid for a specific property until the page is computed. If --size: red is used in width: var(--size), the declaration becomes invalid at computed-value time. The property then falls back to its inherited or initial value, and the fallback inside var() is not used. Debug this in DevTools by checking the computed panel.

10. Limits You Should Know

  • Variables cannot be used inside media query conditions, for example @media (min-width: var(--bp)) does not work. Use an env() or a build-time value instead.
  • Variables cannot build a property name or a selector, only a value.
  • A variable cannot be used to create a URL piece inside url() by concatenation. Store the whole url(...) instead.
  • A variable that refers to itself or creates a loop becomes invalid.
  • Container style queries, written as @container style(--theme: dark), let a component react to a variable value. Check browser support before using them in production.

11. Design Tokens and Component APIs

Design systems use two layers of variables. Global tokens hold raw values such as --blue-600. Semantic tokens give them a role such as --color-primary: var(--blue-600). A component can also expose a small public API: a button uses background: var(--btn-bg, var(--color-primary)) so the page can customize it with one line and no extra class.

12. Performance and Good Practice

  • Changing a variable on the root invalidates styles for the whole page. For fast effects such as pointer tracking, set the variable on the smallest element that needs it.
  • Do not create hundreds of unused variables.
  • Always provide fallbacks for variables that may be missing in third party components.
  • Document your variable names in a single place.

13. Accessibility Notes

  • Check color contrast for every theme, not only the default one.
  • Keep spacing and font size variables in rem so user font settings still work.
  • Do not hide important focus styles behind a variable that can be set to transparent.

14. Common Mistakes

  • Forgetting the two dashes in the name.
  • Writing var(brand) instead of var(--brand).
  • Using a variable in a media query condition.
  • Expecting a fallback to fix an invalid value.
  • Naming variables by color value (--red) instead of role (--color-danger).
  • Adding a unit twice, such as calc(var(--gap)px). Use calc(var(--gap) * 1px) for unitless numbers.
  • Declaring variables on a selector that does not contain the elements that use them.

Key Takeaways

CSS variables are live, inherited values. Declare them on :root for global use, override them on any element for local changes, add fallbacks in var(), update them with JavaScript, register them with @property when you need animation, and name them by role to build clean themes and design tokens.

Try it Yourself HTML
Output

Press Run to execute.

Try it Yourself HTML
Output

Press Run to execute.

Try it Yourself HTML
Output

Press Run to execute.

Try it Yourself HTML
Output

Press Run to execute.

Try it Yourself HTML
Output

Press Run to execute.

Try it Yourself HTML
Output

Press Run to execute.

Try it Yourself JAVASCRIPT
Output

Press Run to execute.

Exercise: Build a Theme with VariablesHTML

Move every repeated color, radius and spacing value into CSS variables on :root. Then create a .dark class that redefines only the variables and makes the card dark.

Try it Yourself HTML
Output

Press Run to execute.

Show expected output
Colors, radius and spacing come from var(), the :root block holds the default values and .dark overrides only the variables

This is a self-check — compare your result with the expected output above.

Exercise: Create a Button with a Variable APIHTML

Create a .btn that reads --btn-bg, --btn-color and --btn-radius with fallback values. Then make a second button that only sets --btn-bg and --btn-radius inline or in a modifier class to look different.

Try it Yourself HTML
Output

Press Run to execute.

Show expected output
The .btn uses var(--btn-bg, fallback), var(--btn-color, fallback) and var(--btn-radius, fallback), and the second button only overrides the variables

This is a self-check — compare your result with the expected output above.

Was this page helpful?