Overview
The ThemeToggle component provides a button to switch between light and dark themes. It persists the user’s preference in localStorage and displays appropriate sun/moon icons.
Source Location
/src/components/ThemeToggle.astro
Features
- Toggle between light and dark modes
- Sun icon (shown in dark mode)
- Moon icon (shown in light mode)
- Persists preference to localStorage
- Toggles
dark class on document root
- Hover effects
- Accessible with ARIA label
Props
No props required. Fully self-contained.
Code Example
import ThemeToggle from '../components/ThemeToggle.astro';
<ThemeToggle />
Full Source Code
---
---
<button
id="theme-toggle"
class="p-2 rounded-lg hover:bg-slate-200/50 dark:hover:bg-slate-700/50 transition-colors"
aria-label="Toggle dark mode"
>
<!-- Sun icon (shown in dark mode) -->
<svg id="sun-icon" class="w-5 h-5 hidden dark:block text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"></path>
</svg>
<!-- Moon icon (shown in light mode) -->
<svg id="moon-icon" class="w-5 h-5 block dark:hidden text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"></path>
</svg>
</button>
<script>
const themeToggle = document.getElementById('theme-toggle');
themeToggle?.addEventListener('click', () => {
const isDark = document.documentElement.classList.toggle('dark');
localStorage.setItem('theme', isDark ? 'dark' : 'light');
});
</script>
How It Works
1. Icon Display Logic
<!-- Sun icon: hidden by default, shown in dark mode -->
<svg id="sun-icon" class="hidden dark:block">
<!-- Moon icon: shown by default, hidden in dark mode -->
<svg id="moon-icon" class="block dark:hidden">
- Light mode: Moon icon visible (click to enable dark mode)
- Dark mode: Sun icon visible (click to enable light mode)
2. Theme Toggle Script
themeToggle?.addEventListener('click', () => {
// Toggle 'dark' class on <html> element
const isDark = document.documentElement.classList.toggle('dark');
// Save preference to localStorage
localStorage.setItem('theme', isDark ? 'dark' : 'light');
});
3. Initial Theme Detection
The initial theme is typically set in the Layout component’s head script, before the page renders, to prevent FOUC (Flash of Unstyled Content).
Typical layout script (not in this component):
<script is:inline>
// Check localStorage or system preference
const theme = localStorage.getItem('theme') ||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
if (theme === 'dark') {
document.documentElement.classList.add('dark');
}
</script>
Styling Details
<button class="p-2 rounded-lg hover:bg-slate-200/50 dark:hover:bg-slate-700/50 transition-colors">
- Padding: 8px all sides
- Border radius: Large (lg = 8px)
- Hover: Semi-transparent background
- Transition: Smooth color change
Icons
<svg class="w-5 h-5 text-white"> <!-- Sun (dark mode) -->
<svg class="w-5 h-5 text-slate-600"> <!-- Moon (light mode) -->
- Size: 20x20px
- Sun color: White (visible against dark navbar)
- Moon color: Slate-600 (visible against light navbar)
Integration in Navbar
Typically used in the Navbar component:
<div class="flex items-center gap-2">
<LanguageSelector />
<ThemeToggle />
<button id="mobile-menu-btn">...</button>
</div>
Accessibility
- ✅ ARIA label: “Toggle dark mode” for screen readers
- ✅ Keyboard accessible: Standard button behavior
- ✅ Visual feedback: Clear icon change
- ✅ Focus state: Inherits from button styles
Browser Compatibility
- ✅ localStorage: Supported in all modern browsers
- ✅ classList.toggle(): Widely supported
- ✅ CSS dark: variant: Requires Tailwind CSS
Persistence Flow
- User clicks button
- JavaScript toggles
dark class on <html>
- Tailwind CSS applies
dark:* styles throughout the site
- localStorage saves preference as “dark” or “light”
- On next visit, layout script reads localStorage and applies theme before render
Customization
Different Icons
Replace SVG paths:
<!-- Sun icon -->
<svg viewBox="0 0 24 24">
<circle cx="12" cy="12" r="5" />
<line x1="12" y1="1" x2="12" y2="3" />
<!-- ... more lines for rays -->
</svg>
<!-- Moon icon -->
<svg viewBox="0 0 24 24">
<path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z" />
</svg>
Icon Animation
Add rotation on toggle:
<svg class="w-5 h-5 transition-transform duration-300 dark:rotate-180">
Custom colors:
<button class="p-2 rounded-full bg-primary-100 dark:bg-primary-900 hover:scale-110 transition-all">
Theme Values
Use different theme identifiers:
// Store as 'theme-dark' / 'theme-light'
localStorage.setItem('theme-preference', isDark ? 'theme-dark' : 'theme-light');
Testing Theme Toggle
Manual Testing
- Click the toggle button
- Verify icon changes (moon ↔ sun)
- Check page colors change
- Open DevTools → Application → Local Storage
- Verify
theme key is “dark” or “light”
- Refresh page - theme should persist
Programmatic Toggle
// In browser console
document.getElementById('theme-toggle').click();
// Check current theme
localStorage.getItem('theme'); // "dark" or "light"
// Manually set theme
document.documentElement.classList.add('dark');
localStorage.setItem('theme', 'dark');
Common Issues
Flash of Wrong Theme
Problem: Page loads in light mode then switches to dark.
Solution: Ensure layout has inline script in <head> before body content:
<script is:inline>
const theme = localStorage.getItem('theme');
if (theme === 'dark') {
document.documentElement.classList.add('dark');
}
</script>
Icons Not Changing
Problem: Icon doesn’t change when clicking.
Solution: Verify Tailwind’s dark: variant is configured:
// tailwind.config.js
module.exports = {
darkMode: 'class', // Must be 'class' not 'media'
// ...
}