What is CSS variables (custom properties)?
CSS variables (custom properties): CSS variables, or custom properties, are author-defined properties named with two leading dashes whose values you reuse with var() and that inherit.
A custom property is any property whose name starts with --. You declare it like any other property and read it with var(). Unlike Sass or Less variables, which are replaced at build time, custom properties are live in the browser: they follow the cascade, inherit down the DOM tree, can be overridden in a selector or media query, and can be changed from JavaScript at runtime. That makes them the natural carrier for design tokens, themes and any value that depends on context.
How it works
:root {
--accent: oklch(0.53 0.215 283);
--radius: 0.5rem;
}
.dark {
--accent: oklch(0.7 0.165 285);
}
.button {
background: var(--accent);
border-radius: var(--radius, 6px);
}
- Scope follows the cascade. A value declared on
:rootapplies everywhere; one declared on.cardapplies inside cards and overrides the root value there. - Fallbacks.
var(--radius, 6px)uses6pxwhen--radiusis not defined. - Resolution is late. The browser substitutes the value where it is used, at computed-value time, so
calc(var(--space) * 2)works and updates when--spacechanges. - Names are case sensitive.
--Accentand--accentare different properties. - JavaScript access.
el.style.setProperty("--lx", "120px")writes one;getComputedStyle(el).getPropertyValue("--lx")reads it.
Common pitfalls
An unregistered custom property is untyped, so the browser cannot interpolate it: transitioning --angle from 0deg to 90deg jumps instead of animating. Register it with @property and a syntax such as "<angle>" to make it animatable.
A reference to an undefined variable with no fallback makes the whole declaration invalid at computed-value time. The property then falls back to its inherited or initial value, not to an earlier declaration in your stylesheet, which surprises people who expect normal CSS error recovery. Finally, changing variables on :root every frame forces style recalculation across the whole page, so scope rapidly updated values to the smallest subtree that needs them.
How MiniDev UI uses them
Every color, shadow and radius in MiniDev UI is a custom property, and Tailwind CSS v4 maps them to utilities through @theme. Dark mode and the four materials only redefine variables, never components. LightProvider goes further: it writes --lx, --ly, --sx, --sy and --la on <html> from the pointer position, and every shadow in the kit reads them, so all shadows fall away from one light. The box shadow generator and the guide to shadows from one light source show the technique in detail.