CSS custom properties: declaration, usage, fallbacks, scope, and dynamic theming patterns
Variables are declared with -- prefix inside a selector
:root {
--primary: #3b82f6;
--spacing-md: 1rem;
--border-radius: 0.5rem;
}Reference a custom property with var()
.button {
background: var(--primary);
padding: var(--spacing-md);
border-radius: var(--border-radius);
}Provide a fallback if the variable is not defined
.card {
color: var(--text-color, #111111);
background: var(--card-bg, var(--surface, white));
/* nested fallback: tries --card-bg, then --surface, then white */
}:root makes variables available everywhere in the document
:root {
--color-brand: #6366f1;
--font-sans: "Inter", sans-serif;
}Variables defined in a selector only apply to that element and its children
.card {
--card-padding: 1.5rem;
padding: var(--card-padding);
}
.card.compact {
--card-padding: 0.75rem; /* override for compact variant */
}Child elements can override inherited custom properties
:root { --text: #111; }
.dark { --text: #fff; }
.sidebar { --text: #555; }
p { color: var(--text); } /* each context gets the right value */Update a CSS variable from JavaScript
// Set on :root
document.documentElement.style.setProperty("--primary", "#ef4444");
// Set on a specific element
const el = document.querySelector(".card");
el.style.setProperty("--card-padding", "2rem");Read a CSS variable value from JavaScript
const style = getComputedStyle(document.documentElement);
const primary = style.getPropertyValue("--primary").trim();
console.log(primary); // "#3b82f6"Use media query or class to switch themes
:root {
--bg: #ffffff;
--text: #111111;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #0f172a;
--text: #f1f5f9;
}
}
/* Or with a class: */
.dark { --bg: #0f172a; --text: #f1f5f9; }Build a design token system with semantic naming
:root {
/* Primitives */
--blue-500: #3b82f6;
--blue-600: #2563eb;
/* Semantic tokens */
--color-primary: var(--blue-500);
--color-primary-hover: var(--blue-600);
--color-surface: #ffffff;
--color-text: #111827;
}Use custom properties in calc() expressions
:root {
--base-spacing: 0.25rem;
--sidebar-width: 16rem;
}
.main {
padding: calc(var(--base-spacing) * 4); /* 1rem */
width: calc(100% - var(--sidebar-width));
}Change variable values at different breakpoints
:root {
--font-size-base: 1rem;
--spacing-lg: 2rem;
}
@media (min-width: 768px) {
:root {
--font-size-base: 1.125rem;
--spacing-lg: 3rem;
}
}Complete light/dark theme using custom properties
/* themes.css */
:root {
--color-bg: #ffffff;
--color-surface: #f8fafc;
--color-text: #0f172a;
--color-text-muted: #64748b;
--color-primary: #3b82f6;
--color-primary-fg: #ffffff;
--color-border: #e2e8f0;
--shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.dark {
--color-bg: #0f172a;
--color-surface: #1e293b;
--color-text: #f1f5f9;
--color-text-muted: #94a3b8;
--color-primary: #60a5fa;
--color-primary-fg: #0f172a;
--color-border: #334155;
--shadow: 0 1px 3px rgba(0,0,0,0.5);
}Use local variables for component variants
/* Button with CSS variable variants */
.btn {
--btn-bg: var(--color-primary);
--btn-fg: var(--color-primary-fg);
--btn-py: 0.5rem;
--btn-px: 1rem;
--btn-radius: 0.375rem;
background: var(--btn-bg);
color: var(--btn-fg);
padding: var(--btn-py) var(--btn-px);
border-radius: var(--btn-radius);
}
.btn.btn-lg {
--btn-py: 0.75rem;
--btn-px: 1.5rem;
}
.btn.btn-danger {
--btn-bg: #ef4444;
--btn-fg: #ffffff;
}Toggle dark mode by updating a CSS variable on :root
// React theme toggle hook
function useTheme() {
const [theme, setTheme] = useState(() => {
return localStorage.getItem("theme") || "light";
});
useEffect(() => {
const root = document.documentElement;
if (theme === "dark") {
root.classList.add("dark");
} else {
root.classList.remove("dark");
}
localStorage.setItem("theme", theme);
}, [theme]);
const toggle = () => setTheme(t => t === "light" ? "dark" : "light");
return { theme, toggle };
}Prefix component variables with the component name (--btn-bg, --card-padding) to avoid conflicts
Always provide a fallback value for variables that might not be defined: var(--color, #default)
Use CSS variables for design tokens — they are the ideal bridge between design systems and code
CSS variables are inherited — changing a variable on a parent automatically updates all children
CSS variables are live — changing them via JavaScript updates the UI instantly without a page reload