
Theming with CSS variables in Tailwind CSS
Learn how to implement dynamic theming in your Tailwind CSS projects using CSS variables. This comprehensive guide covers setup, implementation, and best practices for creating flexible theme systems.
October 13, 2025 ⢠6 min read ⢠TailwindcssCSSTheming
In this post, I'll show you how to implement powerful theming capabilities in your Tailwind CSS projects using CSS variables for dynamic, runtime theme switching.
TLDR; Entire code is available here: https://codesandbox.io/p/sandbox/747fvp
You can also import this code block in the UiBun Visual Editor
Why CSS Variables for Theming?
CSS variables (custom properties) offer several advantages for theming:
- Runtime switching: Change themes without page reload
- Performance: No need to regenerate CSS files
- Flexibility: Support for custom user themes
- Accessibility: Easy integration with system preferences
Setting Up CSS Variables
First, let's define our CSS variables. We'll set them up on the :root selector for our default theme:
:root {
--color-primary: 59 130 246;
--color-secondary: 236 72 153;
--color-accent: 251 146 60;
--color-background: 255 255 255;
--color-foreground: 15 23 42;
--color-muted: 241 245 249;
--color-border: 226 232 240;
}
[data-theme="dark"] {
--color-primary: 96 165 250;
--color-secondary: 244 114 182;
--color-accent: 251 191 36;
--color-background: 15 23 42;
--color-foreground: 248 250 252;
--color-muted: 30 41 59;
--color-border: 51 65 85;
}
Notice I'm using RGB values without the rgb() wrapper - this is important for Tailwind CSS configuration.
Configuring Tailwind CSS
Next, we need to configure our tailwind.config.js to use these CSS variables:
module.exports = {
content: ["./src/**/*.{js,jsx,ts,tsx,html}"],
darkMode: 'class',
theme: {
extend: {
colors: {
primary: 'rgb(var(--color-primary) / <alpha-value>)',
secondary: 'rgb(var(--color-secondary) / <alpha-value>)',
accent: 'rgb(var(--color-accent) / <alpha-value>)',
background: 'rgb(var(--color-background) / <alpha-value>)',
foreground: 'rgb(var(--color-foreground) / <alpha-value>)',
muted: 'rgb(var(--color-muted) / <alpha-value>)',
border: 'rgb(var(--color-border) / <alpha-value>)',
}
}
},
plugins: []
}
The <alpha-value> placeholder allows us to use opacity modifiers like bg-primary/50.
Creating a Theme Switcher
Let's create a theme switcher using plain HTML and JavaScript:
<button id="theme-toggle" class="p-2 rounded-lg bg-muted border border-border hover:bg-accent transition-colors" aria-label="Toggle theme">
<span id="theme-icon">š</span>
</button>
<script>
const themeToggle = document.getElementById('theme-toggle');
const themeIcon = document.getElementById('theme-icon');
// Load saved theme or default to light
const savedTheme = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-theme', savedTheme);
updateThemeIcon(savedTheme);
themeToggle.addEventListener('click', () => {
const currentTheme = document.documentElement.getAttribute('data-theme');
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
updateThemeIcon(newTheme);
});
function updateThemeIcon(theme) {
themeIcon.textContent = theme === 'light' ? 'š' : 'āļø';
}
</script>
Interactive Demo
Try out the interactive demo below to see the theming system in action. You can toggle between light and dark modes and see how the CSS variables dynamically update the colors:
Using Themed Colors in Components
Now we can use our themed colors throughout our HTML components:
<!-- Card Component -->
<div class="bg-background border border-border rounded-lg p-6 shadow-sm">
<h3 class="text-foreground text-lg font-semibold mb-2">Card Title</h3>
<p class="text-muted-foreground mb-4">This is a card description that uses themed colors.</p>
<div class="space-y-2">
<!-- Card content goes here -->
</div>
</div>
<!-- Button Variants -->
<button class="px-4 py-2 rounded-md font-medium transition-colors bg-primary text-white hover:bg-primary/90">
Primary Button
</button>
<button class="px-4 py-2 rounded-md font-medium transition-colors bg-secondary text-white hover:bg-secondary/90">
Secondary Button
</button>
<button class="px-4 py-2 rounded-md font-medium transition-colors bg-accent text-white hover:bg-accent/90">
Accent Button
</button>
<button class="px-4 py-2 rounded-md font-medium transition-colors border border-border hover:bg-muted">
Outline Button
</button>
Advanced Theming with Multiple Themes
You can extend this system to support multiple themes beyond just light and dark:
:root {
--color-primary: 59 130 246;
--color-secondary: 236 72 153;
/* ... default theme colors */
}
[data-theme="dark"] {
--color-primary: 96 165 250;
--color-secondary: 244 114 182;
/* ... dark theme colors */
}
[data-theme="ocean"] {
--color-primary: 6 182 212;
--color-secondary: 59 130 246;
--color-accent: 34 197 94;
--color-background: 240 249 255;
--color-foreground: 7 89 133;
--color-muted: 224 242 254;
--color-border: 125 211 252;
}
[data-theme="sunset"] {
--color-primary: 239 68 68;
--color-secondary: 245 158 11;
--color-accent: 236 72 153;
--color-background: 255 251 235;
--color-foreground: 127 29 29;
--color-muted: 254 243 199;
--color-border: 252 211 77;
}
System Theme Detection
To automatically detect and use the user's system theme preference:
<script>
// Function to detect and apply system theme
function applySystemTheme() {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const savedTheme = localStorage.getItem('theme');
function updateTheme(e) {
const systemTheme = e.matches ? 'dark' : 'light';
// Only apply system theme if user hasn't manually set a preference
if (!savedTheme) {
document.documentElement.setAttribute('data-theme', systemTheme);
}
}
// Apply initial theme
updateTheme(mediaQuery);
// Listen for system theme changes
mediaQuery.addEventListener('change', updateTheme);
// Return cleanup function
return () => mediaQuery.removeEventListener('change', updateTheme);
}
// Initialize system theme detection
applySystemTheme();
</script>
Theming with Animation
Add smooth transitions when switching themes:
:root {
--color-primary: 59 130 246;
/* ... other color variables */
--transition-colors: color 0.3s ease, background-color 0.3s ease, border-color 0.3s ease;
}
* {
transition: var(--transition-colors);
}
Custom Theme Builder
For an advanced use case, you could create a theme builder that allows users to customize colors:
<div class="space-y-4 p-6 bg-background border border-border rounded-lg">
<h3 class="text-foreground font-semibold mb-4">Custom Theme Builder</h3>
<div class="space-y-4">
<div class="space-y-2">
<label class="text-foreground capitalize">Primary Color</label>
<input
type="color"
id="primary-color"
class="w-full h-10 rounded cursor-pointer"
value="#3b82f6"
/>
</div>
<div class="space-y-2">
<label class="text-foreground capitalize">Secondary Color</label>
<input
type="color"
id="secondary-color"
class="w-full h-10 rounded cursor-pointer"
value="#ec4899"
/>
</div>
<div class="space-y-2">
<label class="text-foreground capitalize">Accent Color</label>
<input
type="color"
id="accent-color"
class="w-full h-10 rounded cursor-pointer"
value="#fb923c"
/>
</div>
</div>
</div>
<script>
// Convert hex to RGB
function hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ?
`${parseInt(result[1], 16)} ${parseInt(result[2], 16)} ${parseInt(result[3], 16)}` :
'0 0 0';
}
// Update CSS variable when color changes
function updateColor(colorName, value) {
const rgb = hexToRgb(value);
document.documentElement.style.setProperty(`--color-${colorName}`, rgb);
}
// Add event listeners to color inputs
document.getElementById('primary-color').addEventListener('input', (e) => {
updateColor('primary', e.target.value);
});
document.getElementById('secondary-color').addEventListener('input', (e) => {
updateColor('secondary', e.target.value);
});
document.getElementById('accent-color').addEventListener('input', (e) => {
updateColor('accent', e.target.value);
});
</script>
Best Practices
- Organize variables: Group related colors together and use consistent naming
- Provide good contrast: Ensure all themes meet accessibility standards
- Test thoroughly: Test all theme combinations in different browsers
- Use semantic naming: Name colors by their purpose (primary, text-bg) rather than appearance (blue, red)
- Graceful fallback: Provide sensible defaults if CSS variables aren't supported
Finally
CSS variables combined with Tailwind CSS create a powerful theming system that's both flexible and maintainable. This approach allows for runtime theme switching without sacrificing the utility-first benefits of Tailwind.
I hope this helps you create amazing themeable interfaces! Subscribe to the UiBun newsletter to get updates about product and new blog posts.
