The same brand colour written forty times across a stylesheet creates a simple problem: changing it one day means finding it forty times, with a real risk of missing one. A preprocessor like Sass used to solve that before the browser ever saw the CSS, but only at build time. Native CSS custom properties solve the same problem directly inside the browser, at any moment.
They differ from preprocessor variables on two points that change everything: their scope follows the normal rules of CSS, and their value can change afterwards, including from JavaScript.
Definition
A CSS custom property, commonly called a CSS variable, is declared with two dashes followed by a name, and read with the var() function. An optional fallback value, after a comma, applies if the property is not defined.
:root {
--brand-color: #e8511d;
--spacing: 16px;
}
.button {
background-color: var(--brand-color);
padding: var(--spacing, 12px); /* 12px if the variable is missing */
}The name is case-sensitive and can contain dashes; --brand-color and --Brand-Color refer to two different properties.
A scope that follows inheritance, not a copy-paste
A Sass variable is replaced by its value at compile time: once the CSS file is generated, it is gone, with no trace left in the browser. A CSS custom property stays a real, living CSS property, following the same CSS inheritance rules as color: declared on an ancestor, it is visible to every descendant, unless one of them redefines it.
.dark-card {
--text: white;
}
.dark-card p {
color: var(--text); /* inherits the parent's value */
}That inheritance-based scope allows redefining a property for a single component or section, without touching its global value declared on :root.
Changeable from JavaScript, no rebuild needed
Where changing a Sass variable means re-running the build, a CSS custom property can be changed at any moment from JavaScript, with element.style.setProperty('--name', 'value'), and the change applies instantly, with no page reload and no regenerated file.
/* on the JavaScript side */
document.documentElement.style.setProperty("--brand-color", "#1a73e8");A typical use case: light and dark theme
Light and dark theming is the most common use: a handful of properties cover the background and text colours, redefined once for the active theme, instead of duplicating every colour rule across the site. Our HTML and CSS course walks through this exact light and dark theme example, showing how to organise a project's custom properties from the very first line of CSS.
:root {
--bg: white;
--text: #111;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #111;
--text: white;
}
}
body {
background: var(--bg);
color: var(--text);
}var() only kicks in when the property is not defined at all. A property that is defined but holds an invalid value for where it is used (a word instead of a colour, for instance) does not fall back to it: the whole declaration becomes invalid instead, which is a different failure and gets debugged differently.Frequently asked questions
No, a custom property cannot act as a condition inside @media: media queries are evaluated before custom properties get resolved. It remains perfectly usable, though, in the declarations written inside the block.
A Sass variable disappears at build time and has no existence in the browser: it follows neither the cascade nor inheritance. A CSS custom property exists at runtime, inherits like a normal property, and can be read or changed afterwards, including from JavaScript.
The property using it falls back to its inherited value if it CSS inheritance, otherwise to its initial value. Nothing breaks the page, but the visual result can be surprising if a specific colour was expected.