Danny 🎠 6 days ago
I LOVE THE NEW DESIGN SO MUCH AAAAAAAAAAH
nick 🤞 6 days ago
the new designs are finally here! still a few rough edges here and there, but we'll smooth them out. if you spot any bugs, tell us <3

Theme Switcher

Written by nick • 11.02.2026

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