Reference guide · html-css-js · Published 2026-08-16 · 3 min read
CSS custom properties
CSS custom properties explained, including how they cascade, how they differ from Sass variables, and where they fix real theming problems.
- ·How variables work
- ·Cascade and scope
- ·Theming patterns
What a custom property is
A CSS custom property is a name you define with two leading dashes, for example --brand-color, whose value the browser stores and later substitutes wherever var(--brand-color) appears. The name declares nothing; it is a bucket. That indirection is exactly what makes it powerful: one well-named property can drive colour, spacing, or a whole skin of a component, and the value can change per component, per section, or per breakpoint without touching the rule bodies.
:root {
--brand-color: #0a5bd3;
--gutter: 1rem;
}
.btn {
background: var(--brand-color);
padding: var(--gutter);
}
If --brand-color is later redefined inside a specific area, var(--brand-color) resolves to the new value there. That is inheritance, not magic, and it is the mental model to keep.
How they differ from a preprocessor variable
Sass and Less variables are replaced at build time: the compiled CSS has the literal value, and nothing at runtime can change it. A CSS custom property is resolved by the browser at computed-value time, so it can react to media queries, container queries, classes toggled by JavaScript, and user settings. Think of preprocessor variables as constants in the shipped stylesheet and custom properties as live, cascading slots.
The practical price is runtime resolution: a page that changes thousands of custom properties on a theme toggle may need to be mindful about how many rules pull var(). In normal site work the overhead is invisible.
Cascade, inheritance and scoping
Custom properties participate in the same cascade as regular properties, but there is one wrinkle: by default a custom property inherits. Defining --brand-color on a parent means every descendant sees it unless a closer ancestor redefines it. Scope is just a declarative tree:
.card { --radius: 8px; }
.card * { border-radius: var(--radius); }
A value that does not exist anywhere is "guaranteed invalid", and any property consuming var() with no fallback falls back to the property's initial value. The recommended escape is a fallback:
.btn { color: var(--brand-color, #333); }
Where they replace patterns
- Theming a whole site. One
:rootblock of tokens, toggled by a class orprefers-color-schemequery, changes palette everywhere without parallel selectors. - Component-level variation. A status pill reads
--pill-bgand--pill-textfrom the element that uses it, so one base rule serves many states. - Handling dynamic JavaScript values. Width of a progress bar, position of a spotlight, or a section offset can be written to a custom property with
style.setProperty('--offset', n + 'px'). The stylesheet keeps the rule; the page supplies the number. - Dark mode without a rebuild. Overriding the token set on a theme class is the current standard technique.
When to use it and when not to
Use var() where a value is shared across rules or driven by a state change: brand colors, spacing scale, radii, and border widths. Do not define a custom property for a single-use literal; that just adds an indirection with no payout.
If a component does the same thing with inline styles, re-read the inline style vs css article before choosing; custom properties give most of the dynamic benefit with the styling staying in the stylesheet. And once a design token is in place, the rest of the page can rely on it safely, especially at responsive breakpoints, where changing --gutter at a media query restyles the spacing scale for the whole page from one token.