In the ever-evolving landscape of web development, creating flexible, maintainable, and dynamic user interfaces is a top priority. Cascading Style Sheets (CSS) have come a long way, and one of the most powerful additions in recent years is CSS Variables (officially called Custom Properties). CSS Variables enable developers to define reusable values, streamline styling workflows, and build dynamic interfaces that adapt to user preferences, screen sizes, and interactions—all without rewriting large chunks of CSS.
Whether you’re building a simple website or a complex web application, mastering CSS Variables can transform how you write and maintain styles. In this guide, we’ll dive deep into what CSS Variables are, how they work, and how to use them to create dynamic, responsive, and user-centric designs.
Table of Contents
-
- What Are CSS Variables?
- Syntax: Declaring and Using Variables
- CSS Variables vs. Preprocessor Variables (Sass/Less)
-
- Declaring Variables: Global vs. Local Scope
- Using Variables with
var() - Fallback Values
-
Dynamic Styling with CSS Variables
- Responsive Design: Media Queries
- User Interaction:
:hover,:focus, and:active - Dark/Light Mode Toggle
-
JavaScript Integration: Supercharging Dynamism
- Reading CSS Variables with JavaScript
- Modifying Variables Dynamically
- Practical Examples: Color Pickers, Sliders, and Animations
-
- Naming Conventions
- Avoiding Overuse
- Performance Considerations
- Browser Support
What Are CSS Variables?
CSS Variables (officially named Custom Properties for Cascading Variables) are entities defined by CSS authors that contain specific values to be reused throughout a document. They follow the cascading rules, meaning their values can be inherited and overridden based on the CSS cascade, making them dynamic and context-aware.
Unlike static values hardcoded into properties (e.g., color: #3498db), CSS Variables act as reusable placeholders, allowing you to update styles globally by changing a single variable.
Syntax: Declaring and Using Variables
CSS Variables are declared using a double hyphen (--) prefix, followed by a name (e.g., --primary-color). They are assigned values like any other CSS property:
/* Declaration */
:root {
--primary-color: #3498db; /* Blue */
--font-size: 16px;
--spacing: 20px;
}
To use a variable, reference it with the var() function:
/* Usage */
.button {
background-color: var(--primary-color);
font-size: var(--font-size);
padding: var(--spacing);
}
Here, :root is a pseudo-class representing the root element of the document (typically <html>). Declaring variables in :root makes them globally scoped, accessible to all elements in the document.
CSS Variables vs. Preprocessor Variables (Sass/Less)
If you’re familiar with preprocessors like Sass or Less, you might wonder how CSS Variables differ from preprocessor variables (e.g., $primary-color in Sass). The key distinctions are:
| Feature | CSS Variables | Preprocessor Variables |
|---|---|---|
| Dynamic | Live-updatable (via CSS/JS) | Compiled to static values |
| Cascade | Obeys CSS cascade (inherited/overridden) | No cascading (static at compile time) |
| Accessibility | Accessible via JavaScript | Not accessible post-compilation |
| Scope | Global (:root) or local (elements) | Module/file-level (Sass) |
For example, a Sass variable like $primary-color: #3498db is compiled to #3498db in the final CSS. If you want to change it dynamically (e.g., on user interaction), you’d need to recompile the Sass. CSS Variables, by contrast, can be updated in real time with CSS or JavaScript—no recompilation needed.
Basic Usage: Getting Started
Declaring Variables: Global vs. Local Scope
Variables can be declared in two scopes:
Global Scope
Declare variables in :root to make them available to the entire document. :root has higher specificity than the <html> tag, ensuring variables are accessible globally:
:root {
--primary-color: #3498db;
--secondary-color: #2ecc71;
--text-color: #333;
}
Local Scope
Declare variables on specific elements to limit their scope to that element and its children (via inheritance):
.card {
--card-bg: #fff; /* Local to .card */
--card-shadow: 0 2px 4px rgba(0,0,0,0.1);
background: var(--card-bg);
box-shadow: var(--card-shadow);
}
/* Override for a specific card variant */
.card.highlight {
--card-bg: #f8f9fa; /* Local override */
--card-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
Here, .card.highlight overrides --card-bg and --card-shadow for elements with the highlight class, demonstrating how local variables cascade.
Using Variables with var()
The var() function accepts two arguments: the variable name and an optional fallback value (used if the variable is undefined):
/* Basic usage */
.button {
color: var(--text-color);
background: var(--primary-color);
}
/* With fallback value */
.alert {
border-left: 4px solid var(--alert-color, #e74c3c); /* Fallback to red if --alert-color is undefined */
}
Fallbacks are useful for backward compatibility or ensuring styles degrade gracefully if a variable is missing.
Dynamic Styling with CSS Variables
CSS Variables truly shine when used to create dynamic, responsive interfaces. Let’s explore common use cases.
Responsive Design: Media Queries
Easily adapt styles across screen sizes by updating variables in media queries. For example, adjust spacing or font size on mobile:
:root {
--font-size: 16px;
--spacing: 20px;
}
/* Mobile devices */
@media (max-width: 768px) {
:root {
--font-size: 14px; /* Smaller font on mobile */
--spacing: 15px; /* Tighter spacing */
}
}
body {
font-size: var(--font-size);
margin: var(--spacing);
}
Now, when the viewport is smaller than 768px, --font-size and --spacing automatically update, and all elements using these variables will reflect the change.
User Interaction: :hover, :focus, and :active
Animate or modify styles on user interaction by overriding variables in pseudo-classes:
.button {
--button-bg: #3498db;
--button-bg-hover: #2980b9;
--button-shadow: 0 2px 4px rgba(0,0,0,0.1);
background: var(--button-bg);
box-shadow: var(--button-shadow);
transition: all 0.3s ease;
}
.button:hover {
--button-bg: var(--button-bg-hover); /* Override on hover */
--button-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
Here, hovering over .button updates --button-bg and --button-shadow, triggering a smooth transition (thanks to transition: all 0.3s ease).
Dark/Light Mode Toggle
One of the most popular use cases for CSS Variables is implementing dark/light mode. Define theme-specific variables and toggle a class on the root element to switch themes:
Step 1: Define Variables for Both Themes
/* Light theme (default) */
:root {
--bg-color: #ffffff;
--text-color: #333333;
--card-bg: #f5f5f5;
}
/* Dark theme (activated via class) */
:root.dark-mode {
--bg-color: #1a1a1a;
--text-color: #f5f5f5;
--card-bg: #2d2d2d;
}
Step 2: Apply Variables to Elements
body {
background-color: var(--bg-color);
color: var(--text-color);
transition: background-color 0.3s ease, color 0.3s ease;
}
.card {
background-color: var(--card-bg);
transition: background-color 0.3s ease;
}
Step 3: Toggle the Theme with JavaScript
Add a button to switch between modes by toggling the dark-mode class on :root:
<button id="theme-toggle">Toggle Dark Mode</button>
<script>
const themeToggle = document.getElementById('theme-toggle');
themeToggle.addEventListener('click', () => {
document.documentElement.classList.toggle('dark-mode');
// Optional: Save preference to localStorage
const isDarkMode = document.documentElement.classList.contains('dark-mode');
localStorage.setItem('dark-mode', isDarkMode);
});
// Load saved preference on page load
if (localStorage.getItem('dark-mode') === 'true') {
document.documentElement.classList.add('dark-mode');
}
</script>
This approach ensures a seamless theme transition with minimal CSS/JS.
JavaScript Integration: Supercharging Dynamism
CSS Variables are not just for CSS—JavaScript can read, modify, and react to them, enabling even more dynamic behavior.
Reading CSS Variables with JavaScript
Use getComputedStyle() to read the value of a CSS variable:
// Get the root element (<html>)
const root = document.documentElement;
// Read a global variable
const primaryColor = getComputedStyle(root).getPropertyValue('--primary-color');
console.log(primaryColor); // Output: #3498db (or the current value)
Modifying Variables Dynamically
Use setProperty() to update a CSS variable’s value. This change will propagate to all elements using the variable:
// Update --primary-color to green
root.style.setProperty('--primary-color', '#2ecc71');
Practical Examples
Example 1: Color Picker
Let users customize a website’s accent color with an input color picker:
<input type="color" id="accent-picker" value="#3498db">
<script>
const accentPicker = document.getElementById('accent-picker');
accentPicker.addEventListener('input', (e) => {
root.style.setProperty('--primary-color', e.target.value);
});
</script>
Example 2: Slider for Spacing
Adjust padding/margin globally using a range input:
<input type="range" id="spacing-slider" min="10" max="50" value="20">
<script>
const spacingSlider = document.getElementById('spacing-slider');
spacingSlider.addEventListener('input', (e) => {
const spacing = `${e.target.value}px`;
root.style.setProperty('--spacing', spacing);
});
</script>
Example 3: Animating Variables with JavaScript
Create smooth animations by updating variables in a loop (e.g., a pulse effect):
.box {
--box-opacity: 1;
opacity: var(--box-opacity);
transition: opacity 0.1s ease;
}
let opacity = 1;
let decreasing = true;
function pulseBox() {
if (opacity <= 0.5) decreasing = false;
if (opacity >= 1) decreasing = true;
opacity += decreasing ? -0.05 : 0.05;
root.style.setProperty('--box-opacity', opacity);
requestAnimationFrame(pulseBox);
}
pulseBox(); // Start animation
Best Practices & Tips
Naming Conventions
Use clear, consistent names to avoid confusion. Prefix variables with a project or component name (e.g., --header-bg instead of --bg):
/* Good */
:root {
--header-bg: #f8f9fa;
--button-primary-bg: #3498db;
}
/* Avoid (vague names) */
:root {
--bg: #f8f9fa; /* What "bg"? Header? Card? */
--color: #3498db;
}
Avoiding Overuse
While variables are powerful, overusing them can make CSS harder to read. Reserve variables for values reused across components (e.g., colors, spacing, font sizes), not one-off values.
Performance Considerations
Updating CSS variables with JavaScript can trigger reflows/repaints, but modern browsers optimize this efficiently. For complex animations, prefer CSS @keyframes over JS loops when possible.
Browser Support
CSS Variables are supported in all modern browsers (Chrome, Firefox, Safari, Edge). Internet Explorer 11 and earlier do not support them, but you can provide fallbacks for legacy browsers:
/* Fallback for IE11 */
.button {
background-color: #3498db; /* Static fallback */
background-color: var(--primary-color); /* CSS Variable for modern browsers */
}
Check caniuse.com for the latest support data.
Troubleshooting Common Issues
Variables Not Inheriting
If a child element isn’t inheriting a variable, ensure the parent has the variable defined and the child doesn’t override it. Variables inherit by default, but local scopes take precedence.
Variables Not Working in Pseudo-Elements
Variables work in pseudo-elements (e.g., ::before, ::after), but ensure the variable is declared in a scope accessible to the pseudo-element (e.g., the parent element or :root).
Typos in Variable Names
Double-check for typos (e.g., --primary-colour vs. --primary-color). CSS is case-sensitive, and invalid variable names will be ignored.
Conclusion
CSS Variables revolutionize dynamic styling by combining the reusability of preprocessor variables with the flexibility of runtime updates. They simplify responsive design, theme toggling, and user interaction effects, while JavaScript integration unlocks even more possibilities for dynamic UIs.
By adopting CSS Variables, you’ll write cleaner, more maintainable CSS and build interfaces that adapt seamlessly to user needs and contexts.