Skip to main content

ThemeToggle

The ThemeToggle component provides a button to switch between dark and light themes with localStorage persistence and support for Astro’s View Transitions.

Import

---
import ThemeToggle from '@/components/ThemeToggle.astro';
---

Props

isMobile
boolean
default:"false"
Whether this toggle is being used in a mobile menu. When true, the component uses different element IDs to avoid conflicts with the desktop toggle.

Usage

Desktop Header

---
import ThemeToggle from '@/components/ThemeToggle.astro';
---

<header>
  <nav>
    <!-- Other navigation items -->
    <ThemeToggle />
  </nav>
</header>

Mobile Menu

---
import ThemeToggle from '@/components/ThemeToggle.astro';
---

<div class="mobile-menu">
  <!-- Other menu items -->
  <ThemeToggle isMobile={true} />
</div>

Features

LocalStorage Persistence

The component automatically saves the user’s theme preference to localStorage under the key "theme":
localStorage.setItem("theme", "dark");  // or "light"
The theme is restored on page load, preventing flash of unstyled content (FOUC).

Icon Management

The component uses Lucide icons to display the appropriate icon based on the current theme:
  • Dark mode active: Moon icon visible
  • Light mode active: Sun icon visible
Icons are toggled using CSS classes (hidden/block) for smooth transitions.

View Transitions Support

The component is fully compatible with Astro’s View Transitions API. Event listeners are properly re-attached after each navigation:
document.addEventListener("astro:page-load", initialize);

Analytics Tracking

Theme toggles are tracked using Umami analytics with device context:
data-umami-event="Theme Toggle"
data-umami-event-device={device}  <!-- "desktop" or "mobile" -->

Implementation Details

Theme Application

The theme is applied by setting a data-theme attribute on the root <html> element:
document.documentElement.setAttribute("data-theme", "dark");
CSS selectors can then target this attribute:
[data-theme="dark"] {
  /* Dark theme styles */
}

[data-theme="light"] {
  /* Light theme styles */
}

Element IDs

To prevent conflicts between desktop and mobile toggles, the component uses different IDs based on the isMobile prop:
ElementDesktop IDMobile ID
Buttontheme-toggletheme-toggle-mobile
Moon iconmoonmoon-mobile
Sun iconsunsun-mobile

Event Listener Deduplication

The component uses a custom property hasListener to prevent duplicate event listeners from being attached:
if (btn && !btn.hasListener) {
  btn.addEventListener("click", toggleTheme);
  btn.hasListener = true;
}
This is especially important with View Transitions, which may cause the script to run multiple times.

Styling

The component uses utility classes for styling:
<button
  class="theme-toggle-btn inline-flex cursor-pointer items-center gap-2 px-2.5 py-2"
>
  <!-- Icons -->
</button>
You can customize the appearance by overriding the theme-toggle-btn class or adding additional classes.

Accessibility

The button includes an aria-label for screen readers:
aria-label="Cambiar tema"  <!-- "Change theme" in Spanish -->
Consider updating the aria-label to be dynamic based on the current language using the useTranslations utility.

Default Theme

The component defaults to dark mode if no theme is stored in localStorage:
function getTheme() {
  return localStorage.getItem("theme") || "dark";
}

Complete Example

Base.astro
---
import ThemeToggle from '@/components/ThemeToggle.astro';
import Header from '@/components/Header.astro';
---

<!DOCTYPE html>
<html lang="es">
  <head>
    <meta charset="UTF-8" />
    <title>Chapinismos</title>
  </head>
  <body>
    <Header />
    <main>
      <slot />
    </main>
  </body>
</html>
Header.astro
---
import ThemeToggle from '@/components/ThemeToggle.astro';
import Navigation from '@/components/Navigation.astro';
import MobileMenu from '@/components/MobileMenu.astro';
---

<header>
  <!-- Desktop navigation -->
  <nav class="hidden lg:flex items-center gap-4">
    <Navigation />
    <ThemeToggle />
  </nav>
  
  <!-- Mobile menu -->
  <MobileMenu>
    <Navigation />
    <ThemeToggle isMobile={true} />
  </MobileMenu>
</header>
  • Header - Main navigation bar that includes the theme toggle
  • LanguageSwitcher - Companion component for switching languages

CSS Variables

The theme system uses CSS variables defined in src/styles/global.css. See the Design System documentation for a complete list of theme-aware variables.

Build docs developers (and LLMs) love