Lesson 27 of 32
CSS Custom Properties (Variables)
Declaring --custom properties on :root, reading them with var(--name, fallback), the one-line theme-change use case, and how they inherit and can be overridden per scope.
Real stylesheets repeat the same values constantly — the same brand blue in a dozen places, the same spacing unit between a dozen sections. Custom properties (informally called "CSS variables") let you name a value once and reference it everywhere, so changing it later is a one-line edit instead of a find-and-replace across the whole file.
Declaring a custom property
Any property name starting with two dashes is a custom property:
:root {
--brand-color: #3b82f6;
--spacing-unit: 1rem;
}:root is a selector matching the document's root element (<html>), and
it's the conventional place to declare custom properties meant to apply
page-wide — since every other element on the page is a descendant of
<html>, anything declared on :root is reachable from anywhere via
inheritance.
Reading a custom property with var()
.button {
background-color: var(--brand-color);
padding: var(--spacing-unit);
}var(--brand-color) reads the current value of --brand-color in that
element's scope and substitutes it in, as if you'd typed the value directly.
var() also accepts a second argument — a fallback used if the custom
property isn't defined at all:
.button {
color: var(--text-color, #1e293b);
}If --text-color has never been declared anywhere in scope, .button's text
renders #1e293b instead. This is genuinely useful for a component you might
drop into a page that doesn't define every theme variable, or for guarding
against a typo in a variable name breaking a whole rule silently.
The real use case: a one-line theme change
The payoff shows up once a color (or spacing value, or font stack) is used in more than one place. Without custom properties, a rebrand means finding and replacing the same hex code everywhere it was hardcoded:
/* Without custom properties: the same color repeated, and easy to miss one */
.button { background-color: #3b82f6; }
.link { color: #3b82f6; }
.badge { border-color: #3b82f6; }/* With a custom property: one declaration, referenced everywhere */
:root {
--brand-color: #3b82f6;
}
.button { background-color: var(--brand-color); }
.link { color: var(--brand-color); }
.badge { border-color: var(--brand-color); }Change the brand color for the whole site by editing the single :root
declaration — every rule referencing var(--brand-color) updates
automatically. This is the same one-change-many-places idea you already saw
with external stylesheets versus inline styles, one level more granular: now
even a single value inside a stylesheet doesn't need to be repeated by hand.
Custom properties inherit and cascade like normal properties
This is the part worth understanding properly, because it's what makes custom properties more than "named constants" — they behave like real CSS properties, which means the cascade and inheritance rules you already know apply to them directly.
A custom property declared on :root inherits down through every
descendant, same as color or font-family would. And just like any other
inherited property, you can redeclare it on a narrower selector, and that new
value applies only within that selector's scope — to it and its descendants —
without affecting anything outside it:
:root {
--surface-color: white;
}
.card {
background-color: var(--surface-color);
}
.card--dark {
--surface-color: #0f172a; /* overrides, but only inside .card--dark */
color: white;
}Every .card uses whichever --surface-color is in effect for it. A plain
.card inherits the page-wide white from :root. A .card--dark element
redeclares --surface-color on itself, so it and anything nested inside it
see #0f172a instead — while every other card on the page is completely
unaffected, because the override never escapes .card--dark's own scope.
This scoped-override behaviour is what makes custom properties genuinely
useful for theming: define sensible page-wide defaults on :root, then
override just the ones that need to differ, on just the section that needs
them to differ, and every rule that reads the variable via var() picks up
the right value automatically depending on where in the tree it sits.
Try it yourself
--surface-color and --text-color are set once on :root, then overridden
inside .dark-section only. Every rule below just reads var(...) — none of
them hardcode a color a second time.
What to remember
- Custom properties are named with two leading dashes (
--name: value;) and typically declared on:rootfor page-wide scope. var(--name)reads the value;var(--name, fallback)supplies a fallback used when the property is undefined.- The real payoff is a one-line theme change: reference a value everywhere via
var(), and updating the single declaration updates every usage. - Custom properties inherit and cascade like normal CSS properties — redeclaring one inside a narrower selector overrides it only for that selector's scope and its descendants, leaving everything outside untouched.
Check yourself
2 questions · pass 2/2 to unlock Shadows, Borders and Gradients
1.What does
color: var(--brand-color, blue);do if --brand-color has never been declared anywhere?2.A custom property --gap is set to 1rem on :root, then redeclared as 2rem inside .card. What is --gap equal to for an element inside .card?
2 left to answer