Black Weeks. Full Access for 50% OFF. Use code lex50 at checkout.

You'll get every theme we've made — and every one we'll ever make. That's 39 themes total.

Unlimited projects. Lifetime updates. One payment.

Get full access

Building a a multi-theme toggle with Astro and Tailwind CSS v4

Create a sleek theme switcher with default, dark, and accent modes using CSS custom properties and localStorage

Published on July 9, 2025 by Michael Andreuzza

For many web developers, dark mode has become essential for modern web applications, but what if you want to go beyond the basic light/dark toggle? In this tutorial, ya’ll build a sleek a multi-theme switcher that includes default, dark, and accent themes using Astro and Tailwind CSS v4.

Why use a multi-theme toggle?

While most websites take care just light and dark modes ,( Is not like we need more, to be honest…) but adding a third “accent” theme can significantly improve user experience. Or not…depends who visits your site, for eexample, I am just a light mode user…I do not see nothing on a dark mode website only in day time, it’s unreadablde nureadable dark mode only for coding…

This approach gives users more personalization options and can help your brand stand out with a signature color scheme.

Try it on this page!

Setting up the CSS with Tailwind v4 custom properties

First, let’s define our theme variables inside the @theme.In this case ya’ll use OKLCH color values:

@theme {
  /* Styles text */
  --color-primary: oklch(0.07 0 0); 
  /* Styles background */
  --color-secondary: oklch(1 0 0);
  /* For accents */
  --color-accent: oklch(0.67 0.2941 338.67);
}

[data-theme="dark"] {
  /* Styles text */
  --color-primary: oklch(1 0 0);
  /* Styles background */
  --color-secondary: oklch(0.37 0.0073 264.48);
  /* For accents */
  --color-accent: oklch(0.67 0.2941 338.67);
}

[data-theme="accent"] {
  /* Styles text */
  --color-primary: oklch(1 0 0);
  /* Styles background */
  --color-secondary: oklch(0.41 0.1992 291.76);
  /* For accents */
  --color-accent: oklch(0.67 0.2941 338.67);
}

This CSS creates three distinct themes:

  • Default: Light theme with dark text on white background
  • Dark: Classic dark theme with light text on dark background
  • Accent: Purple-tinted theme that offers a unique branded experience

Creating the theme toggle UI

The toggle interface uses three circular buttons, each representing one theme option:

<div class="flex items-center gap-2">
  <!-- Default theme: white base -->
  <button
    data-theme="default"
    class="theme-dot outline-primary/10 size-4 rounded-full bg-white outline-2 transition-all"
    title="Default">
  </button>
  
  <button
    data-theme="dark"
    class="theme-dot outline-primary/10 size-4 rounded-full bg-black outline-2 transition-all"
    title="Dark">
  </button>
  
  <button
    data-theme="accent"
    class="theme-dot outline-primary/10 size-4 rounded-full bg-[#4d22a3] outline-2 transition-all"
    title="Accent">
  </button>
</div>

The Toggles:

Each button uses Tailwind’s utility classes for styling and includes a data-theme attribute that corresponds to our CSS theme names.

Adding the JS part

Here’s the JavaScript that handles theme switching and persistence:

const dots = document.querySelectorAll(".theme-dot");
const savedTheme = localStorage.getItem("theme") || "default";

function applyTheme(theme) {
  if (theme === "default") {
    document.documentElement.removeAttribute("data-theme");
    localStorage.removeItem("theme");
  } else {
    document.documentElement.setAttribute("data-theme", theme);
    localStorage.setItem("theme", theme);
  }
  
  // Toggle ring highlight
  dots.forEach((dot) => {
    const isActive = dot.dataset.theme === theme;
    dot.classList.toggle("ring-offset-2", isActive);
    dot.classList.toggle("ring-current", isActive);
  });
}

// Set theme on load
applyTheme(savedTheme);

// Add click listeners
dots.forEach((dot) => {
  dot.addEventListener("click", () => {
    applyTheme(dot.dataset.theme);
  });
});

This script handles theme application, visual feedback for the active theme, and localStorage persistence.

Preventing flash of unstyled content (FOUC)

To avoid the flash of unstyled content when the page loads, add this script in your document head:

const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
  document.documentElement.setAttribute('data-theme', savedTheme);
}

Conclusion

This multi-theme toggle provides users with more customization options while maintaining clean, maintainable code. The combination of CSS custom properties, Tailwind v4 utilities, and vanilla JS creates a robust theming system that works perfectly with Astro’s.

The OKLCH color space ensures consistent colors across different devices, while localStorage persistence means that the users theme preferences are remembered between sessions.

/Michael Andreuzza

Did you like this post? Please share it with your friends!