CSS Custom Properties (Variables): Complete Guide
CSS custom properties - commonly called CSS variables - are one of the most powerful additions to modern CSS. They let you define reusable values, build dynamic themes, implement dark mode without JavaScript, and create component systems that are easy to maintain. This guide covers everything from the basics to advanced real-world patterns.
The Problem CSS Variables Solve
Before CSS custom properties, managing design tokens across a stylesheet was painful. If your brand color was #7c6cf7 and you used it in 40 places, changing it meant a global search-and-replace. When you needed to support dark mode, you ended up duplicating entire rulesets. Component-level theming required either CSS preprocessors (Sass/Less) or JavaScript manipulation of inline styles.
CSS preprocessor variables like Sass's $primary-color help, but they are compile-time constants - they get baked into the output CSS and cannot be changed at runtime. CSS custom properties are different: they are live CSS values that cascade, inherit, and can be updated dynamically with JavaScript. This distinction is what makes them genuinely powerful.
The issues this creates are real:
- Maintenance burden: A color used in 50 rules requires 50 manual edits to change
- Dark mode complexity: Without custom properties, dark mode requires duplicating every affected rule inside a media query or class
- Component theming: Passing theme values into components required JavaScript or CSS-in-JS overhead
- Responsive values: Adjusting spacing or font sizes at breakpoints meant repeating declarations in every media query
CSS custom properties solve all of these problems natively in the browser, with zero build tooling required.
Basic Syntax: Defining and Using CSS Variables
A CSS custom property name always starts with two dashes (--). You define it like any CSS property and read it back with the var() function:
/* Define variables on the :root element (globally accessible) */
:root {
--color-primary: #7c6cf7;
--color-bg: #ffffff;
--color-text: #1a1a2e;
--spacing-md: 1rem;
--radius-card: 8px;
--font-size-base: 1rem;
}
/* Use them anywhere */
.button {
background: var(--color-primary);
color: #fff;
padding: var(--spacing-md) calc(var(--spacing-md) * 2);
border-radius: var(--radius-card);
font-size: var(--font-size-base);
}
The :root pseudo-class targets the root element of the document (the <html> tag) and has the highest specificity among element selectors, making variables defined there globally available to every element on the page.
Fallback Values
The var() function accepts a second argument as a fallback, used when the variable is not defined:
.card {
/* Uses --card-bg if defined, otherwise falls back to #f5f5f5 */
background: var(--card-bg, #f5f5f5);
/* Fallbacks can reference other variables */
color: var(--card-text, var(--color-text, #333));
}
Fallbacks are essential when building component libraries where consumers may not define every variable your component expects.
Inheritance and the Cascade
CSS custom properties follow the same cascade and inheritance rules as any other CSS property. A variable defined on a parent element is inherited by all its descendants. This is the key difference from Sass variables and is what enables scoped theming:
/* Global defaults */
:root {
--color-primary: #7c6cf7;
--color-bg: #ffffff;
}
/* Override for a specific section */
.section-dark {
--color-bg: #1a1a2e;
--color-text: #e4e4f0;
}
/* This rule uses the same CSS, but renders differently inside .section-dark */
.card {
background: var(--color-bg);
color: var(--color-text);
}
Every .card inside .section-dark automatically picks up the overridden values. You write the component CSS once, and it adapts to its context.
Real-World Example: Dark Mode Without JavaScript
The most compelling use case for CSS custom properties is implementing dark mode using only CSS. The prefers-color-scheme media query, combined with custom properties, gives you a complete dark mode implementation:
:root {
--bg-primary: #ffffff;
--bg-secondary: #f5f5f5;
--text-primary: #1a1a2e;
--text-secondary: #4a4a6a;
--border: #e0e0e0;
--accent: #7c6cf7;
--shadow: rgba(0, 0, 0, 0.08);
}
@media (prefers-color-scheme: dark) {
:root {
--bg-primary: #0f0f23;
--bg-secondary: #1a1a2e;
--text-primary: #e4e4f0;
--text-secondary: #9090b0;
--border: #2a2a3e;
--accent: #9d8fff;
--shadow: rgba(0, 0, 0, 0.4);
}
}
body {
background: var(--bg-primary);
color: var(--text-primary);
}
.card {
background: var(--bg-secondary);
border: 1px solid var(--border);
box-shadow: 0 2px 8px var(--shadow);
}
Now the entire site switches between light and dark mode purely in CSS, respecting the user's OS preference, with zero JavaScript.
User-Toggleable Dark Mode
For a JavaScript-toggled dark mode (where the user clicks a button), use a data attribute on the HTML element instead of the media query:
[data-theme="light"] {
--bg-primary: #0f0f23;
--text-primary: #e4e4f0;
/* ... rest of dark overrides */
}
/* Toggle with JavaScript */
const toggle = () => {
const current = document.documentElement.getAttribute('data-theme');
const next = current === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
localStorage.setItem('theme', next);
};
Step-by-Step: Building a Component Design System
Here is how to structure CSS variables for a real component library:
- Define primitive tokens - raw values with no semantic meaning (e.g., specific hex colors, pixel values)
- Define semantic tokens - meaningful aliases that reference primitives (e.g.,
--color-danger= a red primitive) - Define component tokens - component-specific variables that reference semantic tokens
/* Layer 1: Primitives */
:root {
--purple-500: #7c6cf7;
--purple-600: #6a5ae0;
--red-500: #ef4444;
--gray-100: #f5f5f5;
--gray-900: #111111;
--space-4: 1rem;
--space-8: 2rem;
}
/* Layer 2: Semantic tokens */
:root {
--color-primary: var(--purple-500);
--color-primary-hover: var(--purple-600);
--color-danger: var(--red-500);
--color-surface: var(--gray-100);
--color-text: var(--gray-900);
}
/* Layer 3: Component tokens */
.btn {
--btn-bg: var(--color-primary);
--btn-bg-hover: var(--color-primary-hover);
--btn-padding: var(--space-4);
background: var(--btn-bg);
padding: var(--btn-padding);
cursor: pointer;
transition: background 0.2s;
}
.btn:hover {
background: var(--btn-bg-hover);
}
/* Override at the component level without touching global tokens */
.btn-danger {
--btn-bg: var(--color-danger);
--btn-bg-hover: #dc2626;
}
Minify Your CSS Instantly
After building your variable-driven CSS, minify it for production to remove whitespace and reduce file size. Free, runs in your browser.
Open CSS MinifierJavaScript Interaction: Reading and Writing CSS Variables
CSS custom properties are part of the DOM and can be read and written with JavaScript, enabling dynamic runtime theming:
// Read a CSS variable value
const root = document.documentElement;
const primaryColor = getComputedStyle(root).getPropertyValue('--color-primary').trim();
// Returns: " #7c6cf7"
// Set a CSS variable at runtime
root.style.setProperty('--color-primary', '#ff6347');
// Set on a specific element (scoped override)
const card = document.querySelector('.card');
card.style.setProperty('--card-bg', '#fef3c7');
// Remove a custom property (reverts to inherited value)
card.style.removeProperty('--card-bg');
This is used extensively in drag-to-resize panels, color pickers that preview results live, and user-preference systems that persist choices to localStorage.
Responsive Design with CSS Variables
CSS variables work beautifully with media queries for responsive spacing and typography:
:root {
--spacing-section: 4rem;
--font-size-hero: 3rem;
--grid-columns: 3;
}
@media (max-width: 768px) {
:root {
--spacing-section: 2rem;
--font-size-hero: 2rem;
--grid-columns: 1;
}
}
.section {
padding: var(--spacing-section) 0;
}
.hero h1 {
font-size: var(--font-size-hero);
}
.grid {
display: grid;
grid-template-columns: repeat(var(--grid-columns), 1fr);
}
Instead of duplicating every component's rules inside each media query, you change a handful of variables at the breakpoint and every component adapts automatically.
CSS Variables vs. Sass Variables: Key Differences
- Runtime vs. compile-time: CSS variables exist at runtime and can be changed with JavaScript. Sass variables are resolved at compile time and become static values in the output.
- Cascade and inheritance: CSS variables participate in the CSS cascade. Sass variables do not - they are just text substitution.
- Scope: CSS variables are scoped to the DOM element they are declared on and all descendants. Sass variables are scoped to the file/block they are in.
- Browser support: CSS custom properties have 97%+ global browser support as of 2026. No build step needed.
- Computed values: CSS variables can be used inside
calc(),hsl(), and other CSS functions. Sass variables can only be used in Sass-specific interpolation.
The two are complementary, not competing. Many teams use Sass for its mixins, loops, and file organization, while using CSS custom properties for all design tokens because they need runtime mutability.
Common Mistakes to Avoid
- Forgetting the double dash:
-color-primaryis not a valid custom property. It must start with exactly two dashes:--color-primary. - Invalid fallback syntax:
var(--color, red blue)is valid (the fallback is the entire tokenred blue). Butvar(--color,)with a trailing comma and no fallback value is invalid. - Using variables in media query conditions:
@media (max-width: var(--breakpoint-md))does NOT work. Custom properties cannot be used as media query values - only as property values within rule blocks. - Forgetting whitespace is significant:
--spacing: 1remhas a value of1rem(with a leading space). When used incalc(), this usually still works, but in certain contexts it can cause unexpected behavior. Use--spacing:1rem(no space) if precision matters. - Over-granularizing tokens: Having 200 custom properties for minor one-off values defeats the purpose. Focus on design-system-level tokens: colors, spacing scale, typography, radii, shadows.
Frequently Asked Questions
Do CSS variables work in all browsers?
Yes. CSS custom properties are supported in all modern browsers with approximately 97% global coverage as of 2026. The only notable holdout was Internet Explorer 11, which is now effectively dead. If you need IE11 support, you can use a PostCSS plugin to inline fallback values, but for any modern project, CSS variables are safe to use without polyfills.
Can CSS variables be used inside calc()?
Yes, and this is one of their most powerful features. For example: margin: calc(var(--spacing-base) * 2) or width: calc(100% - var(--sidebar-width)). The variable is resolved first, then calc() performs the arithmetic. This enables fluid spacing systems where you change a single base variable and all calculated values update automatically.
Are CSS variables the same as CSS preprocessor variables?
No. Sass/Less variables are compile-time text substitutions. CSS custom properties are real browser-level values that participate in the cascade, can be inherited, and can be changed at runtime with JavaScript. Sass variables don't exist in the browser's computed style - they are replaced with their literal values before the CSS is delivered to the browser.
Can I use CSS variables for animations?
Yes, but with an important caveat. CSS custom properties themselves are not animatable by default - they don't transition smoothly like color or transform. However, you can register a custom property with @property (the Houdini CSS Properties and Values API) to make it animatable with full type safety. Without @property, animations that change custom property values will snap rather than transition.
How do I organize CSS variables in large projects?
A common pattern is to put all global design tokens in a dedicated tokens.css file imported before all other styles. Group tokens by category: colors, typography, spacing, shadows, radii, breakpoints. Use a three-layer naming convention: primitive (raw values), semantic (meaningful aliases), and component (component-specific overrides). This makes the design system self-documenting and easy to audit.
Can CSS variables be used in SVGs?
Yes, but only in inline SVGs embedded directly in the HTML. SVGs loaded as <img> tags or as CSS background images are sandboxed and cannot access the document's custom properties. For inline SVGs, you can use fill: var(--color-primary) and the SVG will respond to theme changes just like any other element.
Use Our Free CSS Minifier
Once you have built a well-structured CSS variable system, use our free CSS Minifier to strip whitespace and comments from your production stylesheet. A typical CSS file with custom properties minifies 20–40% smaller, reducing page load time without changing any behavior.
Use our free tool here → CSS Minifier at SecureBin.ai
Usman has 10+ years of experience securing enterprise infrastructure, managing high-traffic servers, and building zero-knowledge security tools. Read more about the author.