What are CSS variables?
CSS variables, properly called custom properties, are values you define once and reference anywhere. They look like ordinary properties but start with --, and you read them with the var() function.
/* tokens.css */
:root {
--color-brand: #2563eb;
--radius: 0.5rem;
--space-4: 1rem;
}
.button {
background: var(--color-brand);
border-radius: var(--radius);
padding: var(--space-4) calc(var(--space-4) * 1.5);
}
The key difference from a preprocessor variable is that custom properties are part of the browser’s cascade. They inherit, can be scoped, and can change at runtime. That makes them the natural layer for theming and design tokens.
Declaring and using
Declare a custom property with a name that starts with two dashes and any valid value.
/* declare.css */
:root {
--gap: 1.5rem;
--shadow: 0 1px 2px rgb(0 0 0 / 0.08);
--font-sans: "Inter", system-ui, sans-serif;
}
.card {
gap: var(--gap);
box-shadow: var(--shadow);
font-family: var(--font-sans);
}
Values can be almost anything: colours, lengths, shadows, font stacks, gradients or even fragments of a value. Because they are substituted before the property is parsed, they can be combined with calc().
The cascade and inheritance
Custom properties follow normal CSS rules. A declaration on an element is inherited by its descendants, and the nearest declaration wins.
/* scope.css */
:root {
--accent: #2563eb;
}
.sidebar {
--accent: #f97316;
}
.sidebar a {
color: var(--accent); /* orange inside the sidebar */
}
This is what makes scoping powerful: you can re-theme a component or a section without touching the global tokens. It is also why defining tokens on :root makes them available everywhere.
Fallbacks
var() accepts a fallback as a second argument, used when the property is missing or invalid.
/* fallback.css */
.button {
color: var(--button-color, #ffffff);
background: var(--button-bg, var(--color-brand, #2563eb));
}
Fallbacks are useful for reusable components that should work even when tokens are not defined, and they can be nested. Use them sparingly, though; if a token should always exist, define it clearly rather than hiding the problem.
Theming
Theming is the flagship use case. Swap a set of variables and an entire subtree updates.
/* theme.css */
:root {
--bg: #ffffff;
--surface: #f8fafc;
--text: #0f172a;
--border: #e2e8f0;
}
[data-theme="dark"] {
--bg: #0b0d0c;
--surface: #14161a;
--text: #e5e7eb;
--border: #26292e;
}
body {
background: var(--bg);
color: var(--text);
}
.card {
background: var(--surface);
border: 1px solid var(--border);
}
Toggle a data-theme attribute on the root element and the whole page re-themes instantly, with no duplicated rules. You can also scope a theme to a component, so a dark card can sit on a light page.
Dynamic values with JavaScript
Because custom properties live in the DOM, you can read and write them from JavaScript.
// progress.js
const bar = document.querySelector(".progress");
function setProgress(percent) {
bar.style.setProperty("--progress", `${percent}%`);
}
setProgress(72);
/* progress.css */
.progress__fill {
width: var(--progress, 0%);
}
This is how progress bars, drag interactions, cursor-following effects and theme toggles are commonly built. The value stays in CSS and JavaScript only updates a number.
Typed properties with @property
By default, custom properties are untyped strings, so the browser cannot interpolate them. The @property at-rule registers a type, an initial value and an inheritance behaviour.
/* property.css */
@property --angle {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
.spinner {
background: conic-gradient(from var(--angle), #2563eb, transparent);
animation: rotate 1s linear infinite;
}
@keyframes rotate {
to { --angle: 360deg; }
}
With a registered type, the property becomes animatable and the browser validates the value, which prevents a whole class of bugs.
Best practices
- Define global tokens on
:rootand use them everywhere. - Name tokens by purpose (
--color-text) not appearance (--gray-900). - Override variables for themes and component variants instead of duplicating rules.
- Provide fallbacks for reusable components that may run without tokens.
- Update variables from JavaScript rather than writing inline styles everywhere.
- Register animatable properties with
@property. - Keep the token set small and consistent.
Common mistakes
- Expecting custom properties to behave like Sass variables at build time.
- Using vague names that make the token set hard to maintain.
- Defining every value as a variable and losing readability.
- Forgetting that values are untyped unless registered with
@property. - Overriding tokens globally when only a subtree should change.
- Storing secrets or user data in CSS variables, which are visible in the DOM.
Where to go next
Custom properties are the connective tissue of a maintainable stylesheet. Apply them to layout with Responsive Design, animate typed ones with Animations & Transitions, and understand the cascade behind them in the CSS guide. For build-time variables that complement them, see Sass.