CSS Custom Properties: Variables, Fallbacks, and Themes
CSS custom properties give reusable values a name. A declaration beginning with two dashes stores the value, and the var() function reads it inside another property value. For the broader styling basics, start with the CSS fundamentals. This guide is part of our CSS typography and color guide series.
:root {
--color-accent: #2563eb;
}
.button {
background: var(--color-accent);
} Think of each custom property as a label attached to a CSS value. The label can begin at the document root, then a component or theme can attach a different value to the same label within its own scope.
What Do CSS Custom Properties Do?
CSS custom properties let several declarations reuse a named value and let components or themes override that value. They follow the cascade and normally inherit. Registration with @property can change inheritance and restrict the value type.
The browser keeps the stored value as a sequence of tokens until var() substitutes it into a real declaration. A color label can therefore feed color, background, or border-color, provided that the final substituted value is valid for the receiving property.
Document-wide custom-property defaults usually belong on the :root element. Component-specific values belong on the component container, where inheritance carries them only through the part of the document that needs the override.
Define Shared Values at the Document Root
The :root pseudo-class matches the document's root element, which is html in an HTML page. Values declared there are available to descendants through inheritance.
:root {
--color-text: #1f2937;
--color-accent: #2563eb;
--space-md: 1rem;
--radius-md: 0.5rem;
} Names should describe the value's role when that role is stable. --color-accent remains useful if the accent changes from blue to orange, while --blue becomes misleading after the same edit.
Read a Custom Property Value
Pass the complete custom property name to var(). The browser replaces that function with the computed value available on the current element.
.card {
color: var(--color-text);
border: 1px solid var(--color-accent);
padding: var(--space-md);
border-radius: var(--radius-md);
} Changing --color-accent at the root updates every declaration that reads the label, unless a nearer rule overrides it.
Add a Fallback Value
The fallback argument in var() is used when an unregistered custom property is missing or has an invalid custom-property value, such as a cycle. It does not rescue a value that exists but is wrong for the receiving property, such as 16px used as a color.
.alert {
color: var(--color-danger, #b91c1c);
padding: var(--alert-space, var(--space-md, 1rem));
} The alert uses #b91c1c when --color-danger is undefined. Its padding tries a component value, then the shared spacing label, then a final direct length.
Override a Value in One Scope
Redeclare the same label on a container to change descendants inside that subtree. The rest of the document continues to inherit the root value.
:root {
--color-accent: #2563eb;
}
.warning-panel {
--color-accent: #c2410c;
}
.panel-link {
color: var(--color-accent);
} A panel link inside .warning-panel becomes orange, while the same link class elsewhere remains blue. The label stays the same because its job remains "accent color" in both scopes.
Build a Theme Switch
A theme can override a small set of labels on the root element. Components keep reading the same names, so their rules do not need separate light and dark versions.
:root {
--page-bg: #eef3f0;
--page-text: #1f2937;
--link-color: #1d4ed8;
}
:root[data-theme="dark"] {
--page-bg: #17211d;
--page-text: #e5eee9;
--link-color: #93c5fd;
}
body {
color: var(--page-text);
background: var(--page-bg);
}
a {
color: var(--link-color);
} <html data-theme="dark">
<body>...</body>
</html> Changing the data-theme attribute switches the values inherited by the page. JavaScript can update that attribute after a reader selects a theme, while CSS remains responsible for the visual rules.
How Can JavaScript Read and Change CSS Variables?
JavaScript reads a CSS custom property with getComputedStyle(element).getPropertyValue() and changes it with element.style.setProperty(). Unlike a Sass variable, a custom property remains available in the browser while the page runs.
const root = document.documentElement;
// read the current value
const brand = getComputedStyle(root)
.getPropertyValue("--color-brand")
.trim();
// set a new one
root.style.setProperty("--color-brand", "#7c3aed"); The .trim() is defensive rather than decorative. Browsers have differed over whether the returned value keeps the whitespace that followed the colon in the source, so a comparison against a bare string can fail in one engine and pass in another. Trimming costs nothing and removes the inconsistency.
Calling setProperty() on documentElement writes an inline style on the HTML root. Descendants inherit the value unless another declaration or registration changes that behavior. Set it on a component element when only that component should change.
For an unregistered custom property, a value can be accepted as data but become invalid when another property uses it. Setting --accent: 16px does not produce a useful color. A declaration that tries to use it as a color falls back to the inherited or initial color instead.
Registering a Typed Property
An unregistered CSS custom property does not smoothly interpolate between values. Register its type with @property when you want a transition on that custom property, such as an angle moving from 0deg to 90deg.
@property registers a type and fixes that:
@property --angle {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
.dial {
--angle: 0deg;
transition: --angle 300ms ease;
transform: rotate(var(--angle));
}
.dial:hover {
--angle: 90deg;
} syntax names the type and inherits decides whether the value passes down the tree; both are always required. initial-value is required too for any concrete syntax like <angle>, and is only optional when the syntax is the universal *, which is the case that opts back out of typing. With a real type in place the transition has two endpoints to interpolate between and the rotation sweeps smoothly.
Registration also validates values against the declared type. In this example, inherits: false and initial-value: 0deg provide a defined default when no usable value is available. Check browser support before making a typed-property animation essential to using the page.
Common Pitfalls & Debugging
A Value Becomes Invalid at Computed-value Time
A declaration falls back to its inherited or initial value. The custom property exists, but its substituted value is invalid for the receiving property. Inspect the computed declaration and store a value of the correct type.
:root {
--text-color: 16px;
}
.notice {
color: blue;
color: var(--text-color);
} The browser accepts 16px as custom-property data before it knows where the label will be used. Once substituted into color, the value becomes invalid at computed-value time. A var() fallback does not help here because the custom property exists.
Inheritance Changes More Elements than Expected
A component override affects deeper descendants. Two-dash custom properties inherit by default. Place the override on the narrowest useful container or redeclare the label where inheritance should stop.
Custom Properties Do Not Work in Media Query Conditions
A media query containing var() never matches. The function substitutes values only inside CSS property declarations. Selectors and query conditions cannot use that substitution. Write the breakpoint directly in the condition and use custom properties inside its rules.
/* Invalid */
@media (min-width: var(--breakpoint-md)) {
.sidebar { display: block; }
}
/* Valid */
@media (min-width: 48rem) {
.sidebar { --sidebar-space: 1.5rem; }
} The valid query keeps the breakpoint as a direct length. The custom property remains inside the declaration block, where CSS allows it to store a value for descendant rules.
Name the Values You Reuse
Name values that repeat or change by scope, then let the cascade carry those names to the elements that use them. Keep one-off values directly in their declarations.
Frequently Asked Questions
Are CSS custom properties the same as JavaScript variables?
CSS custom properties hold values that participate in the cascade and are read with var(). JavaScript variables belong to JavaScript and follow different scope rules. Scripts can still read and update CSS custom properties through browser APIs.
Do CSS custom properties inherit?
Unregistered CSS custom properties inherit by default. Registering a property with @property lets you disable inheritance with inherits: false and set an initial value. A declaration on a child can override an inherited custom property.
When should a project use CSS custom properties?
Use CSS custom properties for values that repeat, vary between components, or change together in a theme. Shared colors and spacing are good candidates. A single measurement can stay directly in its declaration when a name adds no clarity.
Can CSS custom properties be set in an inline style attribute?
CSS custom properties can be declared in an HTML style attribute. The value applies to that element and normally inherits to descendants, unless registration disables inheritance or another declaration overrides it. This works well for a per-item accent color supplied by your application.
Can CSS calc() use a custom property?
CSS calc() can use a custom property through var(), as in calc(var(--space-md) * 2). The substituted value must make the expression valid for its destination property, such as a length for padding.
Can CSS custom properties affect performance?
Changing a CSS custom property can trigger style recalculation for elements that depend on it. Broadly inherited values updated repeatedly deserve profiling on large pages. For ordinary colors and spacing, choose useful names and avoid needless updates rather than assuming variables are slow.
Sources
-
[1]
Using CSS custom properties (variables)(developer.mozilla.org)
-
[2]
var() CSS function(developer.mozilla.org)
-
[3]
Introducing the CSS Cascade(developer.mozilla.org)
Read Next
Learn CSS typography and color through color formats, length units, font and text properties, custom properties, and shadows.
The core HTML elements that give a page its shape: the document skeleton, headings and text, lists, links, and images, with small valid examples.
A decision guide to CSS: whether you still need it with Tailwind and AI, Tailwind or plain CSS, what modern CSS does without JavaScript, how long it takes to be useful, and a roadmap to every CSS guide.