As a stylesheet grows, the same values appear again and again: your brand colour in buttons, links and borders; the same spacing in dozens of rules. CSS custom properties, usually called CSS variables, let you name a value once and reuse it everywhere. Change it in one place and the whole site updates. They also make themes and dark mode surprisingly simple.
Declaring and using a variable
A custom property is any property name that starts with two dashes, such as --brand. You declare it inside a rule like any other property. To use it, write var(--brand) wherever a value is expected. Names are case-sensitive, so --Brand and --brand are different variables.
Global variables are usually declared on :root, which matches the <html> element. Because custom properties are inherited, everything on the page can read them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Design tokens on :root</title>
<style>
:root {
--brand: #c2255c;
--brand-dark: #a61e4d;
--text: #212529;
--surface: #fff5f8;
--radius: 10px;
--space: 1rem;
--font: system-ui, sans-serif;
}
body {
font-family: var(--font);
color: var(--text);
padding: calc(var(--space) * 2);
}
.card {
background: var(--surface);
border: 2px solid var(--brand);
border-radius: var(--radius);
padding: var(--space);
margin-block-end: var(--space);
}
.btn {
background: var(--brand);
color: white;
border: 0;
padding: calc(var(--space) / 2) var(--space);
border-radius: var(--radius);
font: inherit;
}
.btn:hover { background: var(--brand-dark); }
</style>
</head>
<body>
<div class="card">
<h3>Jasmine flower subscription</h3>
<p>Fresh malli from Madurai, delivered every morning.</p>
<button class="btn">Subscribe</button>
</div>
</body>
</html>Notice calc(var(--space) * 2). Variables work inside calc(), clamp() and other functions, so you can build a whole spacing scale from one base value.
Fallback values
var() accepts a second argument used when the variable is not defined: color: var(--accent, teal). Fallbacks are useful in reusable components that might be dropped into a page that does not define every variable. Note that a fallback is not a browser-support fallback; it only covers a missing or invalid variable.
Scope and inheritance
Variables follow the cascade and inheritance just like color does. That means you can override a variable for one part of the page by redeclaring it on a more specific element. Every child inside that element sees the new value, while the rest of the page keeps the global one. This is the secret behind component variants:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Overriding variables for variants</title>
<style>
:root { --accent: #1971c2; }
body { font-family: system-ui, sans-serif; padding: 20px; display: flex; gap: 12px; flex-wrap: wrap; }
.btn {
background: var(--accent);
color: white;
border: 2px solid var(--accent);
padding: 10px 18px;
border-radius: 8px;
font: inherit;
}
.btn.success { --accent: #2f9e44; }
.btn.danger { --accent: #e03131; }
.btn.outline {
background: transparent;
color: var(--accent);
}
.sidebar { --accent: #7048e8; display: flex; gap: 12px; }
</style>
</head>
<body>
<button class="btn">Default</button>
<button class="btn success">Confirm</button>
<button class="btn danger">Delete</button>
<button class="btn danger outline">Cancel order</button>
<div class="sidebar"><button class="btn">Inside sidebar</button></div>
</body>
</html>Each variant sets only --accent; the button rule itself never changes. Adding a new colour variant now takes one line.
Dark mode with CSS variables
Themes become a matter of swapping values. Define light colours on :root, then redefine the same variables inside @media (prefers-color-scheme: dark). The rules that use the variables stay exactly the same. Adding color-scheme: light dark also tells the browser to style scrollbars and form controls to match.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Automatic dark mode and a JavaScript toggle</title>
<style>
:root {
color-scheme: light dark;
--bg: #ffffff;
--text: #1f2328;
--muted: #59636e;
--card: #f6f8fa;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #0d1117;
--text: #e6edf3;
--muted: #9198a1;
--card: #161b22;
}
}
body {
background: var(--bg);
color: var(--text);
font-family: system-ui, sans-serif;
padding: 20px;
}
.card { background: var(--card); padding: 16px; border-radius: 10px; }
.muted { color: var(--muted); }
.swatch { width: 100%; height: 12px; border-radius: 6px; background: var(--brand, #f59f00); }
button { font: inherit; margin-block-start: 12px; }
</style>
</head>
<body>
<div class="card">
<h3>Hotel booking: Ooty</h3>
<p class="muted">Switch your system between light and dark mode to see the colours change.</p>
<div class="swatch"></div>
<button onclick="setBrand('#2f9e44')">Green brand</button>
<button onclick="setBrand('#7048e8')">Violet brand</button>
<p id="out" class="muted"></p>
</div>
<script>
function setBrand(colour) {
document.documentElement.style.setProperty('--brand', colour);
document.getElementById('out').textContent = 'Brand colour is now ' + colour;
}
</script>
</body>
</html>The buttons use JavaScript’s style.setProperty() to change --brand on the root element, and the swatch updates instantly. Reading a value works with getComputedStyle(element).getPropertyValue('--brand'). You will learn more about this in JS DOM Manipulation, and the post Dark Mode Design and CSS covers adding a manual theme switch.
Watch out: A variable is just a stored piece of text until it is used. If --size: 20 is used as width: var(--size), the result is width: 20 with no unit, which is invalid, so the property falls back to its initial value without any error message. Store values with units (20px) or multiply in calc(): calc(var(--size) * 1px).
Tip: Variables cannot be used in media query conditions, so @media (width >= var(--bp)) does not work. Keep breakpoints as plain values.
CSS variables vs Sass variables
If you have seen $primary in Sass code, you may wonder why native variables are needed. The difference is when they work. A Sass variable is replaced with its value when the stylesheet is compiled, so the browser never sees it and it cannot change later. A CSS custom property lives in the browser. It follows the cascade, can be different inside each component, responds to media queries and can be changed by JavaScript while the page is open. Today most projects can rely on native custom properties alone.
For advanced cases, the @property rule lets you register a custom property with a type, such as <color> or <length>, an initial value and whether it inherits. Registered properties can even be animated smoothly, which plain variables cannot.
Naming your variables
Name variables by their role, not their value. --color-primary or --surface still make sense when the brand changes from pink to blue; --pink does not. Many teams use the same token names in Figma Variables and in CSS, so designers and developers speak the same language. Well-named CSS variables are the foundation of a design system.
You have now finished the CSS track. From here, combine everything in a small project, then explore the JavaScript track to make your pages interactive.
