Declaring CSS Variables
Custom property names must start with two dashes (--). They are case-sensitive. Declare them inside any CSS rule; the declaration is scoped to that selector.
/* Global variables — declared on :root, available everywhere */
:root {
--primary-color: #1572B6;
--secondary-color: #e44d26;
--font-size-base: 16px;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 32px;
--border-radius: 6px;
--shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
/* Local variable — scoped to .card and its children */
.card {
--card-padding: 24px;
padding: var(--card-padding);
}
Using Variables with var()
Read a variable anywhere in a property value using var(--name). The variable resolves to its declared value at computed time.
button {
background: var(--primary-color);
color: #fff;
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--border-radius);
box-shadow: var(--shadow);
font-size: var(--font-size-base);
}
h1 {
color: var(--primary-color);
margin-bottom: var(--spacing-lg);
}
a {
color: var(--secondary-color);
}
Fallback Values
var() accepts an optional second argument — a fallback used when the variable is not defined. Fallbacks can themselves contain var() calls.
/* var(--name, fallback) */
.btn {
color: var(--btn-color, #fff); /* uses #fff if --btn-color not set */
padding: var(--btn-padding, 10px 20px); /* multi-value fallback */
}
/* Chained fallbacks */
h2 {
font-size: var(--heading-lg, var(--font-size-base, 16px));
/* tries --heading-lg, then --font-size-base, then 16px */
}
Scope and Inheritance
CSS variables follow the same cascade and inheritance rules as other properties. A child element can access any variable declared on an ancestor — including :root. Redeclaring a variable in a nested selector overrides it locally without affecting the rest of the document.
:root {
--color: #1572B6; /* global default */
}
.card {
--color: #e44d26; /* override locally for .card and children */
background: var(--color); /* #e44d26 */
}
.card .title {
color: var(--color); /* still #e44d26 — inherits from .card */
}
h1 {
color: var(--color); /* #1572B6 — uses :root value */
}
Theming with CSS Variables
The most powerful use of custom properties is runtime theming. Define all design tokens on :root, then override them under a theme class or media query. No extra stylesheet download needed.
/* Light theme — default */
:root {
--bg: #ffffff;
--text: #1a1a1a;
--surface: #f5f5f5;
--primary: #1572B6;
--border: #e0e0e0;
}
/* Dark theme — toggled via class or prefers-color-scheme */
[data-theme="dark"],
@media (prefers-color-scheme: dark) {
:root {
--bg: #121212;
--text: #f1f1f1;
--surface: #1e1e1e;
--primary: #4fc3f7;
--border: #333333;
}
}
/* Components just use the tokens — no change needed */
body { background: var(--bg); color: var(--text); }
.card { background: var(--surface); border: 1px solid var(--border); }
a, .btn { color: var(--primary); }
Updating Variables with JavaScript
JavaScript can read and write CSS custom properties at runtime, enabling dynamic theming or user-controlled styles without rewriting CSS classes.
const root = document.documentElement; // or any element
// Read a variable
const primary = getComputedStyle(root).getPropertyValue('--primary-color').trim();
console.log(primary); // e.g. "#1572B6"
// Set a variable
root.style.setProperty('--primary-color', '#e44d26');
// Remove / revert to stylesheet value
root.style.removeProperty('--primary-color');
// Practical: theme switcher
document.querySelector('#themeToggle').addEventListener('click', () => {
const current = root.getAttribute('data-theme');
root.setAttribute('data-theme', current === 'dark' ? 'light' : 'dark');
});
// Practical: user-controlled font size slider
document.querySelector('#fontSlider').addEventListener('input', (e) => {
root.style.setProperty('--font-size-base', e.target.value + 'px');
});
Design Tokens Pattern
Large projects organize CSS variables as design tokens — a single source of truth for every visual value: spacing, color, typography, shadow, radius. Components reference tokens rather than raw values, making global restyling a one-line change.
:root {
/* Primitive tokens */
--color-blue-500: #1572B6;
--color-blue-600: #0f5fa0;
--color-red-500: #e44d26;
--space-1: 4px;
--space-2: 8px;
--space-4: 16px;
--space-8: 32px;
--radius-sm: 4px;
--radius-md: 8px;
--radius-full: 9999px;
/* Semantic tokens — reference primitives */
--color-primary: var(--color-blue-500);
--color-primary-hover: var(--color-blue-600);
--color-danger: var(--color-red-500);
--spacing-component: var(--space-4);
--border-radius-button: var(--radius-sm);
}
/* Now components use semantic tokens — easy to retheme */
.btn-primary {
background: var(--color-primary);
padding: var(--space-2) var(--space-4);
border-radius: var(--border-radius-button);
}
.btn-primary:hover {
background: var(--color-primary-hover);
}
📋 Summary
- Declare with
--name: value;inside any selector. Use:rootfor global variables. - Read with
var(--name)in any property value. var(--name, fallback)— second argument is the fallback when variable is undefined.- Variables are scoped to their selector and inherited by children. Redeclare in a nested scope to override locally.
- Dark mode theming: define tokens on
:root, override under[data-theme="dark"]orprefers-color-scheme: dark. - JavaScript:
getPropertyValue()to read,setProperty()to write,removeProperty()to revert. - Design tokens pattern: primitive tokens → semantic tokens → component styles. One change, global effect.
Frequently Asked Questions
Sass/Less variables are compile-time — they are resolved once during the build step and replaced with their values in the output CSS. The browser never sees the variable names. CSS custom properties are runtime — the browser keeps the variable names and their values, resolves them during rendering, and you can change them with JavaScript or media queries without re-compiling anything. CSS variables also cascade and inherit like normal properties; Sass variables do not.
Yes — calc(var(--spacing-md) * 2) works perfectly. The variable is substituted first, then calc() evaluates. This is extremely useful for spacing systems: declare a base unit like --space: 8px and compute all spacing from it: padding: calc(var(--space) * 2).
Yes — CSS custom properties have been supported in all major browsers since 2016 (Chrome 49, Firefox 31, Safari 9.1, Edge 15). Global support is above 96% as of 2026. The only holdout was Internet Explorer, which is now end-of-life. You can use CSS variables without any polyfill in modern production projects.
Yes — --semantic: var(--primitive); creates a chain. The browser resolves variable references lazily, so a variable can reference another variable declared later in the same rule or declared on the same element via a different rule. Circular references (A references B, B references A) produce an invalid value and the property falls back to its initial value.