Table of Contents
- What Are CSS Custom Properties?
- Syntax and Basic Usage
- Scope and Inheritance
- Dynamic Updates with JavaScript
- Advanced Use Cases
- Best Practices
- Common Pitfalls
- Reference
What Are CSS Custom Properties?
CSS Custom Properties (officially defined in the CSS Variables specification) are user-defined variables that store CSS values for reuse throughout a stylesheet. They are native to CSS, meaning no preprocessing (like Sass or Less) is required—browsers parse them directly.
Key Differentiators from Preprocessor Variables
| Feature | CSS Custom Properties | Preprocessor Variables (Sass/Less) |
|---|---|---|
| Evaluation | Runtime (browser-parsed) | Compile-time (preprocessor-parsed) |
| Dynamic Updates | Possible via CSS/JS | Fixed after compilation |
| Inheritance | Yes (follows CSS cascade) | No (static values) |
| Scope | Scoped to CSS selectors | Global or module-scoped (preprocessor) |
| Browser Support | Modern browsers (no IE) | Any browser (compiled to static CSS) |
For example, you can’t change a Sass variable after your code is compiled, but you can update a CSS Custom Property dynamically with JavaScript—enabling features like live theme switching or responsive adjustments based on user input.
Syntax and Basic Usage
Declaring Custom Properties
Custom properties are declared using a double-dash (--) prefix, followed by a name, and assigned a value. They must be declared within a CSS selector to define their scope.
/* Global scope (applies to all elements) */
:root {
--primary-color: #2c3e50;
--spacing: 1.5rem;
--border-radius: 8px;
}
/* Local scope (applies only to .button elements and their children) */
.button {
--button-bg: var(--primary-color); /* Reuse global variable */
--button-padding: var(--spacing);
}
:rootis a pseudo-class representing the root element of the document (usually<html>). Declaring variables here makes them globally accessible.- Local variables (e.g.,
--button-bgin.button) are scoped to that selector and its descendants.
Using Custom Properties
To use a custom property, reference it with the var() function:
.button {
background-color: var(--button-bg); /* Uses local variable */
padding: var(--button-padding);
border-radius: var(--border-radius); /* Uses global variable */
}
/* Fallback values: If --accent-color is undefined, use #e74c3c */
.card {
border-left: 4px solid var(--accent-color, #e74c3c);
}
The var() function accepts an optional second parameter: a fallback value to use if the custom property is undefined or invalid.
Valid Values
Custom properties accept any valid CSS value: colors, lengths, fonts, strings, or even complex values like url() or calc().
:root {
--base-font: "Inter", sans-serif;
--shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
--content-max-width: 1200px;
--grid-columns: repeat(12, 1fr); /* Valid for CSS Grid */
}
Scope and Inheritance
Custom properties follow the CSS cascade and inheritance rules, meaning:
- A child element inherits custom properties from its parent, unless overridden.
- More specific selectors override less specific ones.
Example: Scope and Inheritance
<div class="parent">
<div class="child">Hello, World!</div>
</div>
/* Parent scope */
.parent {
--text-color: blue;
}
/* Child overrides the parent's --text-color */
.child {
--text-color: red;
color: var(--text-color); /* Uses red (local) */
}
/* If .child didn't declare --text-color, it would inherit blue from .parent */
Dynamic Updates with JavaScript
One of the most powerful features of CSS Custom Properties is their ability to be modified dynamically with JavaScript. This unlocks use cases like theme switching, interactive UIs, and real-time style adjustments.
Accessing Custom Properties
Use getComputedStyle() to read the computed value of a custom property for an element:
const root = document.documentElement; // Represents :root
const primaryColor = getComputedStyle(root).getPropertyValue("--primary-color");
console.log(primaryColor); // Output: #2c3e50
Updating Custom Properties
Use setProperty() to modify a custom property:
// Update global --primary-color
root.style.setProperty("--primary-color", "#3498db");
// Update a local variable on a specific element
const button = document.querySelector(".button");
button.style.setProperty("--button-bg", "#e74c3c");
Example: Theme Switcher
<button id="themeToggle">Toggle Dark Mode</button>
:root {
--bg-color: #ffffff;
--text-color: #2c3e50;
}
/* Dark theme (toggled via JS) */
:root.dark {
--bg-color: #1a2530;
--text-color: #ecf0f1;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
transition: background-color 0.3s, color 0.3s; /* Smooth transition */
}
const themeToggle = document.getElementById("themeToggle");
themeToggle.addEventListener("click", () => {
document.documentElement.classList.toggle("dark");
// Alternatively, update variables directly:
// const isDark = document.documentElement.classList.contains("dark");
// root.style.setProperty("--bg-color", isDark ? "#1a2530" : "#ffffff");
});
Clicking the button toggles the dark class on :root, which updates --bg-color and --text-color—no need to recompile CSS!
Advanced Use Cases
1. Theming (Light/Dark Mode)
As shown earlier, custom properties simplify theming by centralizing color, typography, and spacing values. You can even extend this to support multiple themes (e.g., “vibrant”, “pastel”) by defining theme-specific variable sets.
/* Base variables */
:root {
--spacing: 1rem;
}
/* Light theme */
:root[data-theme="light"] {
--bg-color: #fff;
--text-color: #333;
}
/* Dark theme */
:root[data-theme="dark"] {
--bg-color: #1a1a1a;
--text-color: #fff;
}
/* Vibrant theme */
:root[data-theme="vibrant"] {
--bg-color: #ffeb3b;
--text-color: #d32f2f;
}
2. Responsive Design with Media Queries
Custom properties work seamlessly with media queries to adapt values across screen sizes:
:root {
--column-count: 2; /* Default for mobile */
--spacing: 1rem;
}
/* Tablet */
@media (min-width: 768px) {
:root {
--column-count: 4;
--spacing: 1.5rem;
}
}
/* Desktop */
@media (min-width: 1200px) {
:root {
--column-count: 6;
--spacing: 2rem;
}
}
.gallery {
display: grid;
grid-template-columns: repeat(var(--column-count), 1fr);
gap: var(--spacing);
}
3. Integration with calc()
Combine custom properties with calc() for dynamic calculations:
:root {
--base-font-size: 16px;
--line-height: 1.5;
}
body {
font-size: var(--base-font-size);
line-height: var(--line-height);
}
h1 {
font-size: calc(var(--base-font-size) * 2.5); /* 40px */
margin-bottom: calc(var(--base-font-size) * var(--line-height)); /* 24px */
}
4. Animations and Transitions
While not all CSS properties can be animated directly, you can animate custom properties and use them as intermediaries to drive animations.
:root {
--progress: 0%; /* Animatable custom property */
}
.progress-bar {
height: 8px;
width: var(--progress);
background: var(--primary-color);
transition: --progress 0.5s ease; /* Animate the custom property */
}
/* Trigger animation via JS */
button.addEventListener("click", () => {
root.style.setProperty("--progress", "100%");
});
Note: Browser support for animating custom properties is strong in modern browsers (Chrome 88+, Firefox 75+).
5. Component-Based Design
Encapsulate styles for reusable components by scoping custom properties to the component’s selector. This avoids style leakage and makes components self-contained:
/* Card component */
.card {
--card-bg: #fff;
--card-shadow: 0 2px 4px rgba(0,0,0,0.1);
--card-padding: 1.5rem;
background: var(--card-bg);
box-shadow: var(--card-shadow);
padding: var(--card-padding);
border-radius: 8px;
}
/* Variant: Featured card */
.card.featured {
--card-bg: #f8f9fa;
--card-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
Best Practices
1. Naming Conventions
Use clear, descriptive names to avoid confusion. A common pattern is --[component]-[property] or --[theme]-[property]:
/* Good: Descriptive and scoped */
--button-primary-bg: #3498db;
--card-shadow: 0 2px 8px rgba(0,0,0,0.1);
--text-lg: 1.25rem;
/* Bad: Vague or generic */
--color1: #3498db;
--s: 1rem;
2. Scope Variables Appropriately
- Use
:rootfor global variables (colors, typography, spacing). - Use component selectors (e.g.,
.card) for local variables specific to that component. - Avoid declaring variables in overly broad selectors (e.g.,
body) unless necessary, as this can lead to unintended inheritance.
3. Document Variables
Documenting custom properties helps maintainability, especially in large teams. Use comments or tools like Stylelint with stylelint-config-recommended to enforce documentation:
:root {
/* Primary brand color (used for buttons, links) */
--primary-color: #2c3e50;
/* Base spacing unit (used for margins, padding) */
--spacing: 1rem;
}
4. Avoid Overuse
While custom properties are powerful, overusing them (e.g., declaring a variable for every single value) can make your CSS harder to read. Reserve them for values that are reused, dynamic, or theme-dependent.
Common Pitfalls
1. Forgetting the Double Dash
Custom properties must start with --. Omitting it will result in invalid syntax:
/* Invalid */
:root {
primary-color: #2c3e50; /* Missing -- */
}
/* Valid */
:root {
--primary-color: #2c3e50;
}
2. Invalid Values Break var()
If a custom property is assigned an invalid value (e.g., a color name misspelled as --primary-color: rebeccapurpel), var(--primary-color) will fall back to the element’s inherited value or the browser default. Always validate values!
3. Specificity and Inheritance Issues
Custom properties inherit from parent elements, but a more specific selector will override them. Watch for conflicts:
:root { --text-color: blue; }
div { --text-color: green; } /* Overrides :root for <div> elements */
p { color: var(--text-color); } /* Green inside <div>, blue otherwise */
4. Browser Compatibility
CSS Custom Properties are supported in all modern browsers (Chrome, Firefox, Safari, Edge), but not in Internet Explorer. For legacy support, use fallbacks:
/* Fallback for IE */
.button {
background-color: #2c3e50; /* Static fallback */
background-color: var(--primary-color); /* Custom property for modern browsers */
}
Check caniuse.com for up-to-date compatibility data.
Reference
- MDN Web Docs: CSS Custom Properties
- W3C CSS Variables Specification
- Can I Use: CSS Variables
- CSS Tricks: CSS Custom Properties Guide
Conclusion
CSS Custom Properties are a game-changer for modern CSS development. By enabling runtime updates, scoped styling, and seamless integration with JavaScript, they solve longstanding pain points in maintainability and interactivity. Whether you’re building a simple website or a complex web app, mastering custom properties will elevate your styling workflow—making your code cleaner, more dynamic, and easier to scale.
Start small: define a few global variables for colors and spacing, then experiment with dynamic updates or theming. You’ll quickly wonder how you ever styled without them! 🚀