CSS Variables: Stop Repeating Your Colors
· 4 min read
If you've been following along and building real pages, you've probably already noticed an annoying pattern.
If you've been following along and building real pages, you've probably already noticed an annoying pattern: the same hex code, the same spacing value, or the same font stack gets typed over and over across your stylesheet. Change your brand's blue, and suddenly you're hunting through the entire file for every place that blue appears. CSS custom properties, commonly called CSS variables, solve exactly this problem, and this lesson is about using them properly.
Defining Custom Properties
You define a custom property with two dashes, usually on the :root selector, which represents the top-level html element and makes the variable available everywhere on the page.
:root {
--color-primary: #2563eb;
--color-text: #1e293b;
--spacing-md: 16px;
--font-main: "Helvetica Neue", Arial, sans-serif;
}
You use a variable anywhere with the var() function.
body {
font-family: var(--font-main);
color: var(--color-text);
}
.button-primary {
background-color: var(--color-primary);
padding: var(--spacing-md);
border: none;
border-radius: 6px;
color: white;
}
Now, if the brand color ever changes, you edit one line in :root, and every single element using var(--color-primary) across your entire site updates automatically. This is the whole point: a single source of truth instead of scattered repeated values.
Scoped Variables Explained
This might sound similar to something a preprocessor like Sass does with its own variables, but there's a crucial difference worth knowing: CSS custom properties are a real, native part of the CSS language, understood by the browser itself at runtime, not something converted away before the browser ever sees it. This means they can be changed dynamically, even by JavaScript, and the page updates live, and it also means they respect the cascade and inheritance just like any other CSS, which leads to a genuinely powerful technique: scoped variables.
.card {
--card-background: white;
background-color: var(--card-background);
padding: 20px;
}
.card.dark {
--card-background: #1a1a2e;
}
<div class="card">Vikesh Singh — Light card</div>
<div class="card dark">Tony Stark — Dark card</div>
Both cards use the exact same background-color: var(--card-background); rule, but because .dark redefines the variable's value just for elements with that class, the actual rendered color differs. This pattern, redefining a variable on a more specific selector or class, is the backbone of how most real-world dark mode and theme-switching systems are built in modern CSS, often without a single extra background-color property anywhere else in the file.
You can provide a fallback value as a second argument to var(), used if the variable hasn't been defined at all.
.badge {
background-color: var(--badge-color, gray);
}
If --badge-color was never set anywhere that applies to this element, it falls back to gray instead of the property simply failing silently, which is a safer default behaviour than beginners often expect.
Building A Dark Mode Toggle
A genuinely practical use of variables is a full dark mode toggle, often combined with a data attribute on the page's root element.
:root {
--bg: #ffffff;
--text: #1a1a1a;
}
[data-theme="dark"] {
--bg: #121212;
--text: #f0f0f0;
}
body {
background-color: var(--bg);
color: var(--text);
transition: background-color 0.2s, color 0.2s;
}
Adding data-theme="dark" to the <html> tag (typically done with a small bit of JavaScript when a user clicks a toggle button) instantly swaps every variable's value, and because body references the variables rather than hardcoded colors, the whole page's theme flips without touching dozens of individual rules.
Common Variable Mistakes
A common beginner mistake is defining variables but then continuing to hardcode the same raw values elsewhere out of habit, which defeats the purpose; the real discipline is routing every repeated value, especially colors, spacing, and fonts, through a variable from the very start of a new project, not retrofitting it later once dozens of hardcoded values already exist.
Another mistake is forgetting that custom property names are case-sensitive and must start with two literal dashes; --Color-Primary and --color-primary are treated as two entirely different variables, which can cause confusing "my variable isn't working" bugs that are actually just a typo in casing.
Variables With Media Queries
Variables also pair naturally with the media queries from the previous lesson. Instead of overriding many individual properties inside a breakpoint, you can sometimes just override one or two variables, and let every rule that already references them adjust automatically, which keeps responsive overrides shorter and easier to reason about as a project grows.
:root {
--container-padding: 16px;
}
@media (min-width: 900px) {
:root {
--container-padding: 32px;
}
}
.container {
padding: var(--container-padding);
}
Practical Tips And Exercise
Practical tip: at the start of any new project, spend five minutes defining a small :root palette, spacing scale, and font stack before writing any component styles. It feels like a small delay, but it consistently pays for itself the first time a design decision changes.
Put this into practice: define :root variables for a primary color, a text color, and a spacing value, then build a card for Iron Man using only those variables. Add a .dark class that redefines the color variables, apply it to a second copy of the card, and watch both cards update from the same underlying CSS rules.
KEEP READING