CSS Variables with JavaScript: Dynamic Theming and Runtime Changes
Why Use CSS Variables with JavaScript?
CSS custom properties aren't just for static values in your stylesheet. Because they live in the browser (unlike preprocessor variables), you can read and modify them with JavaScript at runtime. This enables dynamic theming, user preferences, and interactive style changes.
Reading CSS Variables with JavaScript
Getting a Variable Value
// Get a CSS variable from the root element
const root = document.documentElement;
const primaryColor = getComputedStyle(root).getPropertyValue('--primary-color');
console.log(primaryColor); // "#3498db"
// Get from a specific element
const card = document.querySelector('.card');
const cardPadding = getComputedStyle(card).getPropertyValue('--card-padding');Important Notes
- getComputedStyle() returns the computed value after inheritance and cascading
- The returned value includes any leading whitespace — use .trim() if needed
- The variable must be defined on the element or an ancestor
Setting CSS Variables with JavaScript
Setting on Root (Global)
// Set a global CSS variable
document.documentElement.style.setProperty('--primary-color', '#e74c3c');
document.documentElement.style.setProperty('--font-size-base', '18px');
document.documentElement.style.setProperty('--spacing-md', '2rem');Setting on Specific Elements
// Set a variable on a specific element
const card = document.querySelector('.card');
card.style.setProperty('--card-bg', '#f0f0f0');
card.style.setProperty('--card-shadow', '0 4px 12px rgba(0,0,0,0.15)');Removing a Variable
document.documentElement.style.removeProperty('--primary-color');Dark Mode Toggle
One of the most popular use cases — toggling between light and dark themes:
:root {
--bg-color: #ffffff;
--text-color: #333333;
--card-bg: #f8f9fa;
--border-color: #dee2e6;
--primary: #3498db;
}
:root.dark-theme {
--bg-color: #1a1a2e;
--text-color: #e0e0e0;
--card-bg: #16213e;
--border-color: #2a2a4a;
--primary: #5dade2;
}
body {
background: var(--bg-color);
color: var(--text-color);
transition: background 0.3s, color 0.3s;
}
.card {
background: var(--card-bg);
border: 1px solid var(--border-color);
}// Toggle dark mode
const toggleButton = document.querySelector('.theme-toggle');
toggleButton.addEventListener('click', () => {
document.documentElement.classList.toggle('dark-theme');
// Save preference
const isDark = document.documentElement.classList.contains('dark-theme');
localStorage.setItem('theme', isDark ? 'dark' : 'light');
});
// Load saved preference on page load
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'dark') {
document.documentElement.classList.add('dark-theme');
}Dynamic Color Picker
Let users choose their own accent color:
:root {
--user-color: #3498db;
}
.btn-primary { background: var(--user-color); }
a { color: var(--user-color); }
.heading-accent { border-left: 4px solid var(--user-color); }const colorPicker = document.querySelector('#color-picker');
colorPicker.addEventListener('input', (e) => {
document.documentElement.style.setProperty('--user-color', e.target.value);
});Mouse-Tracking Effects
Create elements that respond to mouse position:
.spotlight {
background: radial-gradient(
circle at var(--mouse-x, 50%) var(--mouse-y, 50%),
rgba(255,255,255,0.1),
transparent 50%
);
}document.addEventListener('mousemove', (e) => {
const x = (e.clientX / window.innerWidth) * 100;
const y = (e.clientY / window.innerHeight) * 100;
document.documentElement.style.setProperty('--mouse-x', x + '%');
document.documentElement.style.setProperty('--mouse-y', y + '%');
});Scroll-Based Styles
Change styles based on scroll position:
.header {
background: rgba(255, 255, 255, var(--header-opacity, 0));
box-shadow: 0 2px 4px rgba(0, 0, 0, calc(var(--header-opacity, 0) * 0.1));
}window.addEventListener('scroll', () => {
const scrolled = Math.min(window.scrollY / 200, 1);
document.documentElement.style.setProperty('--header-opacity', scrolled);
});Responsive Font Size Slider
Let users control the base font size:
:root {
--base-font-size: 16px;
}
body {
font-size: var(--base-font-size);
}const fontSlider = document.querySelector('#font-size-slider');
fontSlider.addEventListener('input', (e) => {
document.documentElement.style.setProperty(
'--base-font-size',
e.target.value + 'px'
);
localStorage.setItem('fontSize', e.target.value);
});
// Restore saved size
const savedSize = localStorage.getItem('fontSize');
if (savedSize) {
document.documentElement.style.setProperty('--base-font-size', savedSize + 'px');
fontSlider.value = savedSize;
}Animation Control
Use CSS variables to control animation parameters dynamically:
.animated-box {
animation: bounce var(--duration, 1s) var(--easing, ease) infinite;
transform: scale(var(--scale, 1));
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(var(--bounce-height, -20px)); }
}// Speed control
document.querySelector('#speed').addEventListener('input', (e) => {
document.documentElement.style.setProperty('--duration', e.target.value + 's');
});
// Bounce height control
document.querySelector('#height').addEventListener('input', (e) => {
document.documentElement.style.setProperty('--bounce-height', -e.target.value + 'px');
});Performance Considerations
- Changing CSS variables on :root triggers a repaint of the entire page
- For better performance, set variables on specific elements when possible
- Use requestAnimationFrame for high-frequency updates (mouse tracking, scroll)
// Better performance for frequent updates
document.addEventListener('mousemove', (e) => {
requestAnimationFrame(() => {
document.documentElement.style.setProperty('--mouse-x', e.clientX + 'px');
document.documentElement.style.setProperty('--mouse-y', e.clientY + 'px');
});
});Summary
CSS variables combined with JavaScript create powerful dynamic styling capabilities. Use getComputedStyle() to read variables and setProperty() to modify them at runtime. This enables dark mode toggles, user preference controls, mouse-tracking effects, scroll-based styling, and animation parameter control. Store user preferences in localStorage for persistence across sessions.