CSS Variables: Dynamic Styling Made Easy
CSS Variables, officially known as CSS Custom Properties, have revolutionized how we write and maintain stylesheets. Gone are the days of find-and-replace across massive CSS files when updating a color scheme. With CSS Variables, you can define values once and reuse them throughout your entire stylesheet—and even change them dynamically with JavaScript.
This comprehensive guide covers everything from basic syntax to advanced patterns, including theming systems, responsive design, animations, and JavaScript integration. By the end, you'll have the knowledge to build maintainable, dynamic styling systems that scale with your projects.
- CSS Variable syntax and declaration
- Scoping, inheritance, and the cascade
- Building theme systems (dark/light mode)
- Dynamic animations with CSS Variables
- JavaScript integration and live updates
- Real-world patterns and best practices
- Browser support and fallback strategies
Understanding CSS Variables Syntax
CSS Variables use a simple yet powerful syntax. They're defined with a double hyphen prefix (--) and accessed using the var() function. Let's start with the fundamentals.
CSS Variables are declared with -- prefix and consumed using var() function
Basic Declaration and Usage
CSS Variables are typically declared in the :root pseudo-class to make them globally available. However, they can be declared in any selector for scoped usage.
/* Global variables - available everywhere */
:root {
/* Colors */
--primary-color: #264de4;
--secondary-color: #9b59b6;
--background: #ffffff;
--text-color: #1a1a1a;
/* Spacing */
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
--spacing-xl: 32px;
/* Typography */
--font-family: 'Inter', sans-serif;
--font-size-base: 16px;
--line-height: 1.6;
/* Borders & Shadows */
--border-radius: 8px;
--shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* Using the variables */
.button {
background: var(--primary-color);
color: white;
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--border-radius);
font-family: var(--font-family);
box-shadow: var(--shadow);
}
.card {
background: var(--background);
color: var(--text-color);
padding: var(--spacing-lg);
border-radius: var(--border-radius);
}
Fallback Values
The var() function accepts a second parameter as a fallback value. This is crucial for defensive CSS and handling undefined variables.
.element {
/* Simple fallback */
color: var(--undefined-var, #333);
/* Fallback to another variable */
background: var(--accent-color, var(--primary-color, blue));
/* Fallback with complex value */
box-shadow: var(--custom-shadow, 0 4px 12px rgba(0,0,0,0.15));
}
Always provide fallback values for critical styles, especially when variables might be overridden or when supporting older browsers that don't fully support CSS Variables.
Scoping and Inheritance
One of the most powerful features of CSS Variables is their participation in the CSS cascade. Variables can be scoped to specific elements and will inherit down the DOM tree.
CSS Variables follow the cascade and can be overridden at any level
/* Global scope */
:root {
--primary: #264de4;
--text: #1a1a1a;
}
/* Component-scoped override */
.danger-zone {
--primary: #e74c3c; /* Red for this section */
}
/* All buttons use --primary */
.button {
background: var(--primary);
}
/* Button in .danger-zone will be red! */
Building a Theme System
CSS Variables shine when building theme systems. Here's how to implement a robust dark/light mode toggle that's maintainable and performant.
/* Define theme variables */
[data-theme="light"] {
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--text-primary: #1a1a1a;
--text-muted: #666666;
--border-color: #e0e0e0;
--shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
[data-theme="dark"] {
--bg-primary: #000000;
--bg-secondary: #111111;
--text-primary: #ffffff;
--text-muted: #888888;
--border-color: #2a2a2a;
--shadow: 0 2px 8px rgba(255, 255, 255, 0.05);
}
/* Use variables throughout */
body {
background: var(--bg-primary);
color: var(--text-primary);
transition: background 0.3s, color 0.3s;
}
.card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
box-shadow: var(--shadow);
}
<!-- Set default theme on html element -->
<html lang="en" data-theme="dark">
<head>
<!-- Prevent flash by setting theme early -->
<script>
const theme = localStorage.getItem('theme') ||
(window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark' : 'light');
document.documentElement.dataset.theme = theme;
</script>
</head>
<body>
<button id="themeToggle">
Toggle Theme
</button>
</body>
</html>
const themeToggle = document.getElementById('themeToggle');
// Toggle theme function
function toggleTheme() {
const html = document.documentElement;
const currentTheme = html.dataset.theme;
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
html.dataset.theme = newTheme;
localStorage.setItem('theme', newTheme);
}
themeToggle.addEventListener('click', toggleTheme);
// Listen for system preference changes
window.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', (e) => {
if (!localStorage.getItem('theme')) {
document.documentElement.dataset.theme =
e.matches ? 'dark' : 'light';
}
});
Always set the theme in a blocking <script> in the <head> before your CSS loads. This prevents users from seeing a flash of the wrong theme on page load.
Dynamic Animations
CSS Variables can be animated directly with CSS (in supported browsers) or manipulated via JavaScript for smooth dynamic effects.
/* Animatable custom properties (with @property) */
@property --gradient-angle {
syntax: '<angle>';
initial-value: 0deg;
inherits: false;
}
.gradient-border {
--gradient-angle: 0deg;
background: linear-gradient(
var(--gradient-angle),
#264de4,
#9b59b6,
#2ecc71
);
animation: rotate-gradient 3s linear infinite;
}
@keyframes rotate-gradient {
to {
--gradient-angle: 360deg;
}
}
/* Progress bar with variable */
.progress-bar {
--progress: 0%;
width: var(--progress);
transition: width 0.5s ease-out;
}
/* Hover effect with variable */
.card {
--lift: 0;
transform: translateY(calc(var(--lift) * -8px));
box-shadow: 0 calc(var(--lift) * 10px) calc(var(--lift) * 30px) rgba(0,0,0,0.1);
transition: transform 0.3s, box-shadow 0.3s;
}
.card:hover {
--lift: 1;
}
JavaScript Integration
CSS Variables can be read and written from JavaScript, enabling powerful dynamic styling without class toggling or inline styles.
Reading Variables
Use getComputedStyle() to read the current value of any CSS variable from any element.
Writing Variables
Use element.style.setProperty() to dynamically set CSS variable values from JavaScript.
// Reading CSS Variables
const root = document.documentElement;
const styles = getComputedStyle(root);
const primaryColor = styles.getPropertyValue('--primary-color').trim();
console.log(primaryColor); // "#264de4"
// Writing CSS Variables
root.style.setProperty('--primary-color', '#e74c3c');
// Dynamic mouse-following gradient
document.addEventListener('mousemove', (e) => {
const x = (e.clientX / window.innerWidth) * 100;
const y = (e.clientY / window.innerHeight) * 100;
root.style.setProperty('--mouse-x', `${x}%`);
root.style.setProperty('--mouse-y', `${y}%`);
});
// Use in CSS:
// background: radial-gradient(
// circle at var(--mouse-x) var(--mouse-y),
// var(--accent), transparent
// );
// Progress bar update
function updateProgress(percent) {
const progressBar = document.querySelector('.progress-bar');
progressBar.style.setProperty('--progress', `${percent}%`);
}
// Color picker integration
const colorPicker = document.querySelector('input[type="color"]');
colorPicker.addEventListener('input', (e) => {
root.style.setProperty('--user-accent', e.target.value);
});
Responsive Design with Variables
CSS Variables work beautifully with media queries, allowing you to adjust spacing, typography, and layout values at different breakpoints.
/* Base (mobile-first) values */
:root {
--container-width: 100%;
--container-padding: 16px;
--font-size-h1: 2rem;
--font-size-body: 1rem;
--grid-columns: 1;
--spacing-section: 40px;
}
/* Tablet */
@media (min-width: 768px) {
:root {
--container-padding: 24px;
--font-size-h1: 2.5rem;
--grid-columns: 2;
--spacing-section: 60px;
}
}
/* Desktop */
@media (min-width: 1200px) {
:root {
--container-width: 1200px;
--container-padding: 0;
--font-size-h1: 3.5rem;
--font-size-body: 1.1rem;
--grid-columns: 3;
--spacing-section: 100px;
}
}
/* Usage - no media queries needed in components! */
.container {
max-width: var(--container-width);
padding: 0 var(--container-padding);
margin: 0 auto;
}
h1 {
font-size: var(--font-size-h1);
}
.grid {
display: grid;
grid-template-columns: repeat(var(--grid-columns), 1fr);
}
section {
padding: var(--spacing-section) 0;
}
CSS Variables vs Preprocessor Variables
How do CSS Custom Properties compare to Sass/LESS variables? Here's a detailed comparison:
| Feature | CSS Variables | Sass/LESS Variables |
|---|---|---|
| Runtime Changes | ✅ Yes - can change via JS | ❌ No - compiled at build time |
| Cascade & Inheritance | ✅ Full CSS cascade support | ❌ No cascade awareness |
| Scoping | ✅ DOM-aware scoping | ✅ File/block scoping |
| Media Query Changes | ✅ Supported natively | ❌ Requires duplication |
| Calculations | ✅ With calc() | ✅ Native math operators |
| Browser Support | ⚠️ IE11 not supported | ✅ Compiles to standard CSS |
| DevTools Inspection | ✅ Visible in browser | ❌ Variables not visible |
Use both! Sass variables for build-time constants (like breakpoints in mixins), and CSS Variables for runtime theming and dynamic values. They complement each other perfectly.
Real-World Patterns
Let's explore some practical patterns you'll use in real projects.
Component Design Tokens
/* Global tokens */
:root {
/* Primitives */
--color-blue-500: #264de4;
--color-gray-100: #f8f9fa;
--radius-sm: 4px;
--radius-md: 8px;
/* Semantic tokens */
--color-primary: var(--color-blue-500);
--color-surface: var(--color-gray-100);
}
/* Component tokens */
.button {
--button-bg: var(--color-primary);
--button-radius: var(--radius-md);
--button-padding: 12px 24px;
background: var(--button-bg);
border-radius: var(--button-radius);
padding: var(--button-padding);
}
/* Variant override */
.button--small {
--button-padding: 8px 16px;
--button-radius: var(--radius-sm);
}
Fluid Typography
:root {
/* Fluid scale using clamp() */
--font-size-sm: clamp(0.875rem, 0.8rem + 0.25vw, 1rem);
--font-size-base: clamp(1rem, 0.9rem + 0.5vw, 1.125rem);
--font-size-lg: clamp(1.25rem, 1rem + 1vw, 1.5rem);
--font-size-xl: clamp(1.5rem, 1rem + 2vw, 2.5rem);
--font-size-2xl: clamp(2rem, 1rem + 4vw, 4rem);
}
h1 { font-size: var(--font-size-2xl); }
h2 { font-size: var(--font-size-xl); }
p { font-size: var(--font-size-base); }
Color Manipulation with HSL
:root {
/* Store HSL components separately */
--primary-h: 227;
--primary-s: 79%;
--primary-l: 52%;
/* Compose the color */
--primary: hsl(
var(--primary-h),
var(--primary-s),
var(--primary-l)
);
/* Create variants easily! */
--primary-light: hsl(
var(--primary-h),
var(--primary-s),
calc(var(--primary-l) + 15%)
);
--primary-dark: hsl(
var(--primary-h),
var(--primary-s),
calc(var(--primary-l) - 15%)
);
/* Semi-transparent version */
--primary-alpha: hsla(
var(--primary-h),
var(--primary-s),
var(--primary-l),
0.2
);
}
Browser Support
CSS Variables have excellent browser support, covering 97%+ of global users. However, IE11 doesn't support them.
Fallback Strategies
/* Strategy 1: Duplicate declarations */
.button {
background: #264de4; /* Fallback */
background: var(--primary);
}
/* Strategy 2: @supports query */
.card {
background: #ffffff;
}
@supports (--css: variables) {
.card {
background: var(--surface);
}
}
/* Strategy 3: CSS Variables fallback chain */
.element {
color: var(--custom, var(--default, #333));
}
Best Practices Summary
Use Descriptive Names
Name variables by their purpose (--color-primary) not their value (--blue). This makes theming easier and code more readable.
Organize with Prefixes
Group related variables: --color-*, --spacing-*, --font-*. This improves discoverability and prevents naming collisions.
Define at :root for Globals
Use :root for theme-wide variables. Use component selectors for scoped variables that shouldn't leak globally.
Always Provide Fallbacks
Use the second parameter in var() for critical styles. This ensures graceful degradation if a variable is undefined.
Combine with Preprocessors
Use Sass for build-time logic and mixins, CSS Variables for runtime theming. They work great together!
Ready to Transform Your CSS?
CSS Variables are now a fundamental part of modern web development. Start using them today to build more maintainable, flexible, and dynamic stylesheets. Your future self (and your team) will thank you!
