Theme Switcher
Live Preview
While having a dark mode toggle is great, sometimes you want to give your users even more control - like switching between seasonal themes or custom color palettes. Why settle for just two options when you can have as many as you want? Here's how to build a multi-theme switcher!
1. The HTML Structure
Instead of a single toggle, we're using a theme picker. Each button has a data-set-theme attribute that tells our JavaScript exactly which theme to activate.
For this tutorial, we're using simple colored circles as buttons to keep things easy to read. However, you can use whatever you like! You could replace these buttons with text, Font Awesome icons, or even small thumbnail images. As long as the data-set-theme attribute stays on the element, the logic will still work.
<div class="theme-picker">
<!-- "root" removes the theme attribute to show your default design -->
<button class="theme-button button-default" data-set-theme="root" title="Default"></button>
<button class="theme-button button-spring" data-set-theme="spring" title="Spring"></button>
<button class="theme-button button-summer" data-set-theme="summer" title="Summer"></button>
<button class="theme-button button-autumn" data-set-theme="autumn" title="Autumn"></button>
<button class="theme-button button-winter" data-set-theme="winter" title="Winter"></button>
</div>
Notice the data-set-theme="root"? This is your "Reset" button. When clicked, it simply removes the theme attribute so your site falls back to the original :root colors.
It also acts as a safety catch: if a user has an old or broken theme name saved in their browser that doesn't exist in your code anymore, the script will automatically revert to this default state so the design never looks broken.
2. The CSS
To make the theme swap actually work, we use CSS variables. Think of these as "nicknames" for your colors. Your :root defines the default nicknames. Then, you create separate blocks for each theme that give those same nicknames new values. When you switch themes, the browser just updates the definition once, and the whole site changes instantly!
If you're starting a brand new project, you can set these up right away. If you're adding the toggle to a site you've already built, you'll just need to go through your CSS and replace your hardcoded colors (like #ffffff) with these variables (like var(--bg-color)).
<style>
/* 1. Your default theme */
:root {
--bg-color: #f0f0f0;
--text-color: #666666;
--accent-color: #999999;
}
/* 2. Your themes, for example a Spring and Summer theme */
[data-theme='spring'] {
--bg-color: #e8f5e9;
--text-color: #1b5e20;
--accent-color: #4caf50;
}
[data-theme='summer'] {
--bg-color: #e1f5fe;
--text-color: #01579b;
--accent-color: #03a9f4;
}
/* 3. Use the variables instead of hardcoded hex codes */
body {
background-color: var(--bg-color);
color: var(--text-color);
}
/* Container for the theme picker */
.theme-picker {
display: flex;
gap: 15px;
background: rgba(255, 255, 255, 0.5);
padding: 15px;
border-radius: 30px;
width: fit-content;
}
/* Base style for every theme button */
.theme-button {
border: 2px solid transparent;
border-radius: 50%;
width: 40px;
height: 40px;
cursor: pointer;
transition: transform 0.2s ease, border-color 0.2s;
}
.theme-button:hover {
transform: scale(1.1);
}
/* Style for the currently active theme */
.theme-button.active {
border-color: var(--text-color);
transform: scale(1.15);
}
/* Specific colors for the theme picker */
.button-default { background-color: #999999; }
.button-spring { background-color: #4caf50; }
.button-summer { background-color: #03a9f4; }
.button-autumn { background-color: #ff9800; }
.button-winter { background-color: #607d8b; }
</style>
Advanced Customization: You aren't limited to just swapping colors. Since the theme name is applied to the <html> tag, you can change the actual style or layout of any element for a specific theme. For example, if you want a taller header or a specific background image only during summer, you can target it directly:
/* Add a snow background only in winter */
[data-theme='winter'] body {
background-image: url('snow-pattern.png');
}
/* Make the header bigger only in summer */
[data-theme='summer'] #header {
height: 300px;
font-family: 'Comic Sans', cursive; /* Maybe don't actually do this... */
}
3. The JavaScript
We split the script into two parts to prevent a "white flash." This happens when the browser loads your default :root colors for a split second before it realizes you have a different theme saved. By putting the first part in the <head>, the browser can check your preference and apply it before it even starts drawing the page.
The second part goes at the bottom of the <body>. This handles the actual button clicks and updates the theme.
Add this to your <head>:
<script>
// The key name used for saving the preference in localStorage
const storageKey = 'theme-preference';
// Valid named themes and the default setting
const validThemes = ['spring', 'summer', 'autumn', 'winter'];
const defaultTheme = 'root';
// Look for a saved theme in the browser
const savedTheme = localStorage.getItem(storageKey);
// Decide which theme to use when the page first loads
const themeToApply = validThemes.includes(savedTheme) ? savedTheme : defaultTheme;
// Apply the correct theme attribute or remove it for default
if (themeToApply !== 'root') {
document.documentElement.setAttribute('data-theme', themeToApply);
} else {
document.documentElement.removeAttribute('data-theme');
}
</script>
Important: If you decide to add more themes later (like [data-theme='halloween']) in your CSS, don't forget to add the name to the validThemes list in the script above! If it's not in that list, the script will think it's an error and load your default theme instead.
Then, add this at the bottom of your <body>:
<script>
// The <html> element where the theme attribute is applied
const htmlElement = document.documentElement;
// The theme picker buttons
const themeButtons = document.querySelectorAll('.theme-button');
// Apply the correct active state based on the current theme
const currentTheme = htmlElement.getAttribute('data-theme') || 'root';
updateActiveButton(currentTheme);
// Listen for whenever a user clicks a button to swap themes
themeButtons.forEach(button => {
button.addEventListener('click', () => {
const newTheme = button.getAttribute('data-set-theme');
applyTheme(newTheme);
localStorage.setItem(storageKey, newTheme);
});
});
// Function to update the HTML attribute and change the active button state
function applyTheme(theme) {
if (theme === 'root') {
htmlElement.removeAttribute('data-theme');
} else {
htmlElement.setAttribute('data-theme', theme);
}
updateActiveButton(theme);
}
// Function to highlight the active theme button
function updateActiveButton(theme) {
themeButtons.forEach(button => {
button.classList.toggle('active', button.getAttribute('data-set-theme') === theme);
});
}
</script>
4. The full code
To see how everything looks when it's all put together, I've put the complete code below!
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Theme Switcher</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css">
<style>
/* Default (root) colors */
:root {
--bg-color: #f0f0f0;
--text-color: #666666;
--accent-color: #999999;
}
/* Spring theme colors */
[data-theme='spring'] {
--bg-color: #e8f5e9;
--text-color: #1b5e20;
--accent-color: #4caf50;
}
/* Summer theme colors */
[data-theme='summer'] {
--bg-color: #e1f5fe;
--text-color: #01579b;
--accent-color: #03a9f4;
}
/* Autumn theme colors */
[data-theme='autumn'] {
--bg-color: #fff3e0;
--text-color: #e65100;
--accent-color: #ff9800;
}
/* Winter theme colors */
[data-theme='winter'] {
--bg-color: #eceff1;
--text-color: #263238;
--accent-color: #607d8b;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
font-family: Arial, sans-serif;
/* These lines below are just to center the demo on the page! */
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
}
.theme-picker {
display: flex;
gap: 15px;
background: rgba(255, 255, 255, 0.5);
padding: 15px;
border-radius: 30px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.theme-button {
border: 2px solid transparent;
border-radius: 50%;
width: 40px;
height: 40px;
cursor: pointer;
transition: transform 0.2s ease, border-color 0.2s;
}
.theme-button:hover {
transform: scale(1.1);
}
.theme-button.active {
border-color: var(--text-color);
transform: scale(1.15);
}
.button-default { background-color: #999999; }
.button-spring { background-color: #4caf50; }
.button-summer { background-color: #03a9f4; }
.button-autumn { background-color: #ff9800; }
.button-winter { background-color: #607d8b; }
</style>
<script>
// The key name used for saving the preference in localStorage
const storageKey = 'theme-preference';
// Valid named themes and the default setting
const validThemes = ['spring', 'summer', 'autumn', 'winter'];
const defaultTheme = 'root';
// Look for a saved theme in the browser
const savedTheme = localStorage.getItem(storageKey);
// Decide which theme to use when the page first loads
const themeToApply = validThemes.includes(savedTheme) ? savedTheme : defaultTheme;
// Apply the correct theme attribute or remove it for default
if (themeToApply !== 'root') {
document.documentElement.setAttribute('data-theme', themeToApply);
} else {
document.documentElement.removeAttribute('data-theme');
}
</script>
</head>
<body>
<h1 style="margin-bottom: 0;">Theme Switcher</h1>
<p>Click a circle below to see the themes in action!</p>
<div class="theme-picker">
<button class="theme-button button-default" data-set-theme="root" title="Default Root"></button>
<button class="theme-button button-spring" data-set-theme="spring" title="Spring"></button>
<button class="theme-button button-summer" data-set-theme="summer" title="Summer"></button>
<button class="theme-button button-autumn" data-set-theme="autumn" title="Autumn"></button>
<button class="theme-button button-winter" data-set-theme="winter" title="Winter"></button>
</div>
<script>
// The <html> element where the theme attribute is applied
const htmlElement = document.documentElement;
// The theme picker buttons
const themeButtons = document.querySelectorAll('.theme-button');
// Apply the correct active state based on the current theme
const currentTheme = htmlElement.getAttribute('data-theme') || 'root';
updateActiveButton(currentTheme);
// Listen for whenever a user clicks a button to swap themes
themeButtons.forEach(button => {
button.addEventListener('click', () => {
const newTheme = button.getAttribute('data-set-theme');
applyTheme(newTheme);
localStorage.setItem(storageKey, newTheme);
});
});
// Function to update the HTML attribute and change the active button state
function applyTheme(theme) {
if (theme === 'root') {
htmlElement.removeAttribute('data-theme');
} else {
htmlElement.setAttribute('data-theme', theme);
}
updateActiveButton(theme);
}
// Function to highlight the active theme button
function updateActiveButton(theme) {
themeButtons.forEach(button => {
button.classList.toggle('active', button.getAttribute('data-set-theme') === theme);
});
}
</script>
</body>
</html>
Aaaand you're done! You've successfully moved beyond a simple dark mode toggle. <3