Free Online Code Editor
Learn how CSS custom properties work, how scoping and fallbacks behave, and how to build a dark mode toggle with a few lines of CSS and JavaScript.
Open a stylesheet from a real project and you'll often find the same color written forty times. When the brand color changes, you're on a find-and-replace hunt, hoping you didn't miss one.
CSS custom properties, usually called CSS variables, fix that. You define a value once, use it everywhere, and change it in one place.
A variable name starts with two dashes. You read it back with var().
<style>
:root {
--brand: #16a34a;
--radius: 10px;
--space: 16px;
}
.button {
background: var(--brand);
border-radius: var(--radius);
padding: var(--space);
color: white;
border: none;
font-size: 16px;
}
.card {
border: 2px solid var(--brand);
border-radius: var(--radius);
padding: var(--space);
margin-bottom: var(--space);
font-family: sans-serif;
}
</style>
<div class="card">A card using the brand color.</div>
<button class="button">A button using it too</button>:root is the top of the document (the same as <html>), so anything defined there is available everywhere. Change --brand to #2563eb and both the card and button turn blue.
var() accepts a second argument, used if the variable isn't defined.
.note {
color: var(--note-color, #444);
}If --note-color doesn't exist, the text is #444. This is handy for components that should work with or without a theme.
Unlike variables in Sass, which are replaced at build time, CSS variables are live. They inherit down the tree, and you can override them for any part of the page.
<style>
:root { --bg: #f3f4f6; --text: #111827; }
.panel {
background: var(--bg);
color: var(--text);
padding: 16px;
margin-bottom: 12px;
border-radius: 8px;
font-family: sans-serif;
}
.panel.inverted {
--bg: #111827;
--text: #f9fafb;
}
</style>
<div class="panel">Normal panel</div>
<div class="panel inverted">Same CSS, different variables</div>The .panel rule is identical for both. Only the variables changed, and everything inside the inverted panel inherits the new values. This is how you build components that adapt to their surroundings.
Define your colors as variables, then override them when the user prefers dark.
<style>
:root {
--bg: #ffffff;
--text: #1f2937;
--card: #f3f4f6;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #0f172a;
--text: #e2e8f0;
--card: #1e293b;
}
}
body { background: var(--bg); color: var(--text); font-family: sans-serif; padding: 16px; }
.card { background: var(--card); padding: 16px; border-radius: 8px; }
</style>
<h2>Hello</h2>
<div class="card">This page follows your system's light or dark setting.</div>Change your operating system between light and dark and the page switches. You didn't touch a single rule below the variables.
Many sites also let the visitor choose. The usual approach is a data-theme attribute on the <html> element.
<style>
:root { --bg: #ffffff; --text: #1f2937; }
:root[data-theme="dark"] { --bg: #0f172a; --text: #e2e8f0; }
body {
background: var(--bg);
color: var(--text);
font-family: sans-serif;
padding: 16px;
transition: background 0.3s, color 0.3s;
}
</style>
<h2>Theme toggle</h2>
<button id="toggle">Switch theme</button>
<script>
const root = document.documentElement;
document.querySelector("#toggle").addEventListener("click", () => {
const next = root.dataset.theme === "dark" ? "light" : "dark";
root.dataset.theme = next;
});
</script>On a real site, save the choice (for example in localStorage) and apply it before the page paints, so it doesn't flash the wrong theme.
Since they're live, JavaScript can change them at any time. That's perfect for sliders, color pickers, and user settings.
<style>
:root { --size: 24px; }
p { font-size: var(--size); font-family: sans-serif; }
</style>
<input id="range" type="range" min="12" max="48" value="24">
<p>Resize this text</p>
<script>
document.querySelector("#range").addEventListener("input", (event) => {
document.documentElement.style.setProperty("--size", event.target.value + "px");
});
</script>To read the current value, use getComputedStyle(element).getPropertyValue("--size").
Variables work inside calc(), so you can build a whole spacing scale from one base.
:root {
--space: 8px;
}
.stack > * + * { margin-top: calc(var(--space) * 2); }
.section { padding: calc(var(--space) * 4); }Change --space once and the whole rhythm of the page shifts.
Good candidates are things that repeat or might change: colors, font sizes, spacing, border radius, shadows, and transition durations. Name them by purpose (--color-text, --color-surface) rather than by value (--dark-gray), so a theme change doesn't make the names lie.
Don't turn every number into a variable. A one-off margin-top: 3px doesn't need one.
@media (min-width: var(--bp)) doesn't work. The value there is fixed.var(--brnad) doesn't exist, the property becomes invalid and falls back to its inherited or initial value. Check the spelling.--Brand and --brand are different.--name, use with var(--name), and put global ones on :root.prefers-color-scheme or a data-theme attribute.setProperty.Reading helps, but typing the code is what makes it stick. Open the editor, paste any example from this article, and change it until it breaks.
Open the HTML/CSS editorStop guessing between Flexbox and CSS Grid. Learn the one question that decides it, with copy-and-run layouts for navbars, card galleries, and page templates.
Validate forms with plain HTML attributes, style valid and invalid states with CSS, and use the Constraint Validation API for custom messages.
Learn when to use CSS transitions and when to use keyframe animations, which properties stay smooth, and how to respect users who prefer reduced motion.