Introduction
Imagine that your brand colour appears in 80 places and then the brand changes. Without variables, that is 80 risky edits. With custom properties it is one. They are more capable than they first appear, because they obey the cascade, which makes it possible to switch whole colour themes by changing a single attribute.
How it works
Declaring and using. A custom property's name starts with two dashes. You read it using var():
:root {
--brand: teal;
--space: 16px;
}
.button {
background: var(--brand);
padding: var(--space);
}
:root matches the html element, so variables placed there are available everywhere. Names are case sensitive, so --Brand and --brand are different variables.
Fallbacks. var(--accent, crimson) uses crimson if --accent has not been defined.
They inherit. A variable behaves like any inherited property, so a child sees the value from its nearest ancestor. You can therefore change one for a section of the page:
:root { --bg: white; }
.sidebar { --bg: whitesmoke; }
.card { background: var(--bg); }
A card in the sidebar is automatically different from a card elsewhere, without any extra rules for it. This is the idea that makes components flexible.
Doing arithmetic. A variable can hold a number used with calc(): margin: calc(var(--space) * 2). This allows a whole spacing scale to come from one value.
Theming. A theme is just another set of values for the same variables:
:root { --bg: white; --text: #222; }
[data-theme="dark"] { --bg: #111; --text: #eee; }
body { background: var(--bg); color: var(--text); }
Setting data-theme="dark" on html flips everything at once. To follow the visitor's device setting, use a media query:
@media (prefers-color-scheme: dark) {
:root { --bg: #111; --text: #eee; }
}
Compared with preprocessors. Tools such as Sass have variables too, but those are replaced by fixed values when the CSS file is generated. Custom properties live in the browser, so they can change at any moment: through the cascade, through a media query, or from JavaScript with element.style.setProperty("--brand", "navy").
Limits. A variable cannot be used as a property name or as a selector, only as a value (or part of one). An invalid value is not caught when the variable is declared, only when it is used.
Syntax
:root { --radius: 8px; }
.card { border-radius: var(--radius, 4px); }
Example
Line by line
--accentistealfor the whole page..zonechanges it tocrimsonfor itself and its descendants..cardhas only one rule, and it reads whichever value is nearest.
Expected result
Two cards with a coloured bar on the left: teal for the first and crimson for the one inside the zone.
In the real world
Design systems are nearly always built on custom properties, usually called tokens. Dark mode on most modern sites works exactly as shown here. A site's colours can also be changed at run time, for example to apply a customer's brand.
Common mistakes
- Forgetting
var(), as incolor: --brand. - Mismatched case in names.
- Using a variable in a media query condition, which does not work.
- Scattering values and not using the variables you have defined.
Best practices
- Define global tokens on
:root. - Name variables for their purpose:
--text-muted, not--grey-2. - Use a fallback when reading a variable that may be absent.
- Override variables, rather than rewriting whole rules.
Summary
Custom properties store reusable values, and var() reads them. Because they inherit, a section can override a value for its descendants, and a theme is simply a different set of values. They live in the browser and can change at any time.
Quick check
Key takeaways
- Declare with --name and read with var(--name).
- Variables inherit, so they can be overridden in one part of a page.
- A theme is simply the same variables with different values.
- Variables exist at run time, unlike those in a preprocessor.
Practice problems for this lesson
My notes
Write down what you want to remember from this lesson. Notes are saved on this device as you type, and appear on your dashboard.