DEV Community

AjeaS
AjeaS

Posted on

Theme Switcher in Astro project

In short, I created a simple theme switcher using Tailwind CSS and some JS. Here's how I did it.

I've broken it down into 3 sections
global.css
Layout.astro
ThemeToggle.astro

global.css

I defined light theme color variables in the :root by default. Then, for dark mode, I re-defined those same variables for the dark theme. The dark theme only applies if there is a data-theme="dark"attribute set on the HTML tag.

:root { --font-body: DM Sans, sans-serif; --font-fira: Fira Code, monospace; --text-2xl: 2.5rem; --text-xl: 2rem; --text-lg: 1.75rem; --text-md: 1.5rem; --text-sm: 1.25rem; --text-xs: 1.125rem; --text-base: 1rem; --color-neutral-900: #1c1a19; --color-neutral-800: #201e1d; --color-neutral-700: #34302d; --color-neutral-600: #4a4846; --color-neutral-400: #c0bfbf; --color-neutral-300: #dedcda; --color-neutral-200: #efedeb; --color-neutral-100: #fbf9f7; --color-blue-900: #022b4a; --color-blue-800: #5792c0; --color-blue-700: #75b0de; --color-blue-500: #93cefc; --color-blue-200: #e1f1fe; --color-green-900: #132a18; --color-green-700: #008531; --color-green-500: #9dd3a9; --color-green-200: #e9f5ea; --color-yellow-900: #4a3003; --color-yellow-700: #ea9806; --color-yellow-500: #facc79; --color-yellow-200: #fff5e1; --color-red-600: #d92d20; --color-red-400: #f04438; --color-white: #ffffff; /* light mode */ --border: var(--color-neutral-200); --background-color: var(--color-neutral-100); --text-color: var(--color-neutral-600); --heading-text: var(--color-neutral-700); --header-background: var(--color-white); --header-text: var(--color-neutral-800); --menu-toggle-background: var(--color-neutral-800); --menu-toggle-text: var(--color-white); --nav-background: var(--color-white); --nav-text: var(--color-neutral-600); --icon-fill: var(--color-neutral-900); } /* dark mode */ html[data-theme="dark"] { --border: var(--color-neutral-700); --background-color: var(--color-neutral-900); --text-color: var(--color-neutral-400); --heading-text: var(--color-white); --header-background: var(--color-neutral-800); --header-text: var(--color-white); --menu-toggle-background: var(--color-white); --menu-toggle-text: var(--color-neutral-800); --nav-background: var(--color-neutral-800); --nav-text: var(--color-neutral-400); --icon-fill: var(--color-white); } 
Enter fullscreen mode Exit fullscreen mode

NOTE: within my components, I use these color variables so they will switch accordingly when toggling the theme.
ex:

<div className="flex items-center justify-between gap-4 bg-(--header-background) text-(--header-text) transition-colors duration-500 ease-in relative mt-5 mb-3 rounded-[10px] p-[6px] border border-(--border)"> 
Enter fullscreen mode Exit fullscreen mode

Layout.astro

I have a script tag in the head with a is:inline attribute so that this code runs before the HTML is rendered. In a nutshell,

  • I'm getting the theme from localstorage and if it doesn't exist it's set to "light" by default.
  • I'm setting the data-theme attribute to that value.
<html lang="en"> <head> <ClientRouter /> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <meta name="generator" content={Astro.generator} />  <title>Personal Blog</title>  <script is:inline> function applyTheme(doc) { // 1. Get the stored theme preference from localStorage const storedTheme = localStorage.getItem("theme") || "light"; doc.documentElement.setAttribute("data-theme", storedTheme); } // Apply theme on initial page load applyTheme(document); </script>  </head> </html>  
Enter fullscreen mode Exit fullscreen mode

NOTE: I'm using client-side routing with ClientRouter. However, when navigating to a new page (e.g. about) the old DOM is removed (along with its event listeners/data attributes) and the new DOM is created but without event listeners/data attributes attached.

So the data-theme attribute is not attached to the new DOM (about page).

Example: on the Home page, the theme is set to dark mode, if I go to the About page(new DOM everything removed) theme will be set back to default light mode.

FIX: to fix this, I needed to ensure that the data-theme attribute is set on the new DOM upon navigating.

So I used the astro:before-swap event listener to ensure the correct theme is applied to each page upon navigation.

// Apply theme to the new document during Astro View Transitions document.addEventListener("astro:before-swap", (event) => { applyTheme(event.newDocument); }); 
Enter fullscreen mode Exit fullscreen mode

ThemeToggle.astro

This component

  • Sets the data-theme attribute based on localstorage value
  • Displays the correct icons based on theme
  • Saves the theme to localstorage
<script> // Toggle theme icons and set theme attribute function applyTheme(theme: string) { const root = document.documentElement; const moonIcon = document.getElementById("moonIcon"); const sunIcon = document.getElementById("sunIcon"); // Set theme attribute and persist in localStorage root.setAttribute("data-theme", theme); localStorage.setItem("theme", theme); if (!moonIcon || !sunIcon) return; if (theme === "dark") { sunIcon.classList.remove("hidden"); moonIcon.classList.add("hidden"); } else { sunIcon.classList.add("hidden"); moonIcon.classList.remove("hidden"); } } function setUpThemeToggle() { const toggleButton = document.getElementById("themeToggle"); if (!toggleButton) return; // Initialize from saved theme or fallback to light let currentTheme = localStorage.getItem("theme") || "light"; applyTheme(currentTheme); toggleButton.addEventListener("click", () => { currentTheme = currentTheme === "light" ? "dark" : "light"; applyTheme(currentTheme); }); } // runs on page load setUpThemeToggle(); // Reapply theme after Astro page transitions document.addEventListener("astro:after-swap", () => { setUpThemeToggle(); }); </script> 
Enter fullscreen mode Exit fullscreen mode

astro:after-swap fires after page navigations. There I re-fetched the theme from localstorage to ensure the correct icon is displayed. Hope that makes sense.

Lastly, for full context here's the HTML for my theme button

<button id="themeToggle" class="rounded-lg border border-(--border) bg-(--background-color) cursor-pointer transition-colors duration-500 ease-in p-2" > <MoonIcon id="moonIcon" className="w-6 h-6 hidden" /> <Sun id="sunIcon" class="w-6 h-6 hidden" /> </button> 
Enter fullscreen mode Exit fullscreen mode

This is what's currently working for me at the moment. I'm open to suggestions on how to make it better/improve. 👌🙏🏽

Happy Coding!

Top comments (0)