On this page
CSS custom properties are values that participate in the cascade. Unlike build-time variables, they remain available in the browser, inherit through the element tree by default, and can change without generating a new stylesheet. That makes them useful for themes, component options, and runtime values, provided their scope and failure modes are deliberate.
Define a small theme contract
Begin with roles such as surface, text, border, and accent rather than names tied to a particular color. A component needs to know which value represents its surface; it should not need to know whether that surface is white, charcoal, or a brand tint. This semantic contract makes later theme changes much less invasive.
Use the contrast checker to test foreground and background pairs before storing them as custom properties. Check the combinations actually rendered, including muted text and interactive states. A list of individually attractive colors is not a working theme unless their relationships remain usable.
Separate raw palette values from interface roles where that distinction helps your team. A small product may need only a handful of role tokens. A larger product can map those roles onto a shared palette, following the principles in Design Tokens 101. Avoid introducing three naming layers before there is a concrete maintenance problem to solve.
Let scope and inheritance do useful work
Declare shared defaults on :root, then override them on a theme boundary or component. Custom property names are case-sensitive, so consistent spelling matters. The browser resolves var() where the consuming property is computed, which allows an inherited token to take on the appropriate value within a themed subtree.
:root {
--surface: #ffffff;
--text: #18202b;
--border: #ccd3dd;
--accent: #174cbd;
}
[data-theme="dark"] {
color-scheme: dark;
--surface: #151a22;
--text: #f0f3f8;
--border: #536073;
--accent: #9cbcff;
}
.panel {
color: var(--text);
background: var(--surface);
border: 1px solid var(--border);
}A dark section can now contain several components without duplicating each component's rules. This is scoped inheritance, not a global search-and-replace. It also means a deeply nested widget can accidentally inherit host values with the same names. Prefix private component properties when collisions are likely.
Set color-scheme deliberately because it informs browser-rendered controls and other user-agent styling. It does not generate your dark theme or verify its contrast. Review the MDN custom properties guide for the underlying cascade behavior.
Understand fallback and invalid-value behavior
The fallback in var() applies when the referenced custom property is missing or has a guaranteed-invalid value. It does not generally act as validation for an existing token. If --space contains a color, padding: var(--space, 1rem) will not use 1rem simply because the substituted color is inappropriate for padding.
.card {
padding: var(--card-padding, 1rem);
border-radius: var(--card-radius, 0.75rem);
}
.compact-area {
--card-padding: 0.75rem;
}An invalid substitution can invalidate the consuming declaration at computed-value time. The browser does not simply return to an earlier declaration in the cascade as if the bad declaration had never existed. Depending on the property, the outcome follows inherited or initial-value behavior. That distinction explains many confusing fallback bugs.
Store a documented value type for each public token: color, length, number, or complete shadow, for example. Keep units with values when the consuming property requires them. Cyclic references also fail, so avoid chains where a base token ultimately points back to itself through another alias.
For structured values that need validation or animation, consider registered properties. The MDN @property documentation explains syntax constraints, inheritance control, and initial values. Check your supported browsers before relying on registration for essential behavior.
Register properties when typing adds value
A registered property can specify a syntax such as angle or length and can opt out of inheritance. This is useful for a local animation parameter that should not leak into descendants. Registration changes behavior; it is not merely a comment describing a variable.
@property --dial-angle {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
.dial {
transform: rotate(var(--dial-angle));
transition: --dial-angle 180ms ease;
}Here typed interpolation allows the browser to interpolate an angle rather than treating an unregistered custom property as an opaque token sequence. Still consider reduced-motion preferences when the resulting transformation is animated. Typed properties do not exempt an interaction from motion review.
Use registration selectively. Theme colors often benefit from normal inheritance, while private component state may not. A global registry full of narrowly named properties adds maintenance cost, so document which registrations are part of the public styling API and which belong to one implementation.
Remember that custom properties cannot supply media-query conditions in ordinary CSS. A declaration like a reusable breakpoint variable does not make var() usable in a media query. Keep breakpoint orchestration in supported CSS constructs or your build tooling instead of expecting runtime variables to behave like preprocessor substitutions.
Update dynamic values without widening the blast radius
JavaScript can update a property on the smallest relevant element. For a progress display, prefer a local unitless ratio rather than repeatedly rewriting a long transform string. Validate and clamp external input before passing it to the styling layer.
function updateProgress(element, rawValue) {
const value = Number(rawValue);
if (!Number.isFinite(value)) return;
const progress = Math.min(1, Math.max(0, value));
element.style.setProperty('--progress', String(progress));
}.progress-fill {
transform-origin: left;
transform: scaleX(var(--progress, 0));
}This is presentation state, not an accessible progress indicator by itself. Expose the corresponding semantic value through the appropriate HTML element or ARIA pattern. Do not place secrets in custom properties: users can inspect styles, just as they can inspect other client-side data.
Changing an inherited token near the document root can affect many descendants. Profile high-frequency updates and keep rapidly changing values local. A custom property that ultimately changes width can still cause layout; variables do not make expensive properties cheap. The MDN setProperty reference covers the JavaScript API.
Test nested themes, missing tokens, browser controls, and persisted user preferences. Use the dark-mode design guide to review color relationships, and consult HTML and CSS articles for related implementation patterns. Predictable boundaries matter more than having the largest possible token collection.
Key takeaways
- Name theme tokens by role and verify real color pairings.
- Use inheritance intentionally and scope overrides close to their consumers.
- Treat var() fallbacks as missing-value handling, not general type validation.
- Register properties when typing or inheritance control solves a real problem.
- Validate runtime values and profile the properties they ultimately affect.
Reach designers & developers at work
Put your product next to the tools people use every day. One sponsor per placement, no tracking pixels.
Become a sponsor