ThemeSwitcher Component
The ThemeSwitcher component provides an animated toggle button for switching between light and dark themes. It includes smooth transitions, localStorage persistence, and automatic detection of system preferences.
Features
- Animated sun/moon icon transitions
- Smooth theme switching with 500ms transitions
- localStorage persistence across sessions
- System preference detection on first load
- Hover scale effects
- Glassmorphism button design
- Full dark mode support
Source Code Location
src/components/ThemeSwitcher.jsx
Dependencies
import React, { useEffect, useState } from "react";
Usage
import ThemeSwitcher from './components/ThemeSwitcher';
function App() {
return (
<div className="flex items-center gap-2">
<ThemeSwitcher />
</div>
);
}
Component State
The component manages a single state variable:
const [isDark, setIsDark] = useState(false);
Theme Initialization
On component mount, the theme is initialized based on saved preferences or system settings:
useEffect(() => {
const savedTheme = localStorage.getItem("theme");
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
const initialTheme = savedTheme || (prefersDark ? "dark" : "light");
document.documentElement.classList.toggle("dark", initialTheme === "dark");
setIsDark(initialTheme === "dark");
}, []);
Initialization Priority
- localStorage: If a theme is saved, use it
- System preference: If no saved theme, detect OS dark mode setting
- Default: Falls back to light mode if neither is available
Theme Toggle Handler
The handleToggle function manages theme switching:
const handleToggle = () => {
const newTheme = !isDark ? "dark" : "light";
const html = document.documentElement;
html.classList.add("theme-transition");
html.classList.toggle("dark", newTheme === "dark");
setTimeout(() => {
html.classList.remove("theme-transition");
}, 500);
localStorage.setItem("theme", newTheme);
setIsDark(!isDark);
};
Toggle Flow
- Determine new theme (opposite of current)
- Add transition class for smooth animation
- Toggle dark mode class on
<html> element
- Remove transition class after 500ms
- Save preference to localStorage
- Update component state
The button uses a glassmorphism design with overlapping icons:
<button
onClick={handleToggle}
className="relative inline-flex cursor-pointer select-none items-center justify-center p-4 md:p-5 rounded-xl shadow-card bg-white/0.1 backdrop-blur-xs border border-black/30 dark:border-white/20 transition-colors duration-500"
aria-label="Cambiar tema"
>
{/* Sun icon */}
{/* Moon icon */}
</button>
Icon Animation
Both icons are absolutely positioned and toggle between visible/hidden states:
Sun Icon (Light Mode)
<span
className={`absolute flex h-9 w-9 items-center justify-center rounded transition-all duration-300 ease-in-out ${
!isDark
? "opacity-100 text-violet-800 dark:text-violet-400 hover:scale-125 hover:text-violet-900 dark:hover:text-violet-500"
: "opacity-0 scale-75 text-violet-500 dark:text-violet-200"
}`}
>
<svg>{/* Sun icon SVG */}</svg>
</span>
Moon Icon (Dark Mode)
<span
className={`absolute flex h-9 w-9 items-center justify-center rounded transition-all duration-300 ease-in-out ${
isDark
? "opacity-100 text-violet-800 dark:text-violet-400 hover:scale-125 hover:text-violet-900 dark:hover:text-violet-500"
: "opacity-0 scale-75 text-violet-500 dark:text-violet-200"
}`}
>
<svg>{/* Moon icon SVG */}</svg>
</span>
Animation Properties
Transitions between opacity-100 (visible) and opacity-0 (hidden)
Inactive icon is scaled down to scale-75
Active icon scales up on hover for interactive feedback
transition-all duration-300
All property changes animate over 300ms with ease-in-out timing
Styling Classes
p-4 md:p-5: Responsive padding (16px mobile, 20px desktop)
rounded-xl: Large border radius
shadow-card: Custom shadow token
bg-white/0.1: Semi-transparent white background
backdrop-blur-xs: Glassmorphism blur effect
border border-black/30 dark:border-white/20: Adaptive borders
Icon States
- Active:
opacity-100, full scale, violet colors
- Inactive:
opacity-0, scale-75, muted colors
- Hover:
scale-125 for enhanced interaction
Accessibility
The component includes an aria-label for screen readers:
aria-label="Cambiar tema"
Consider localizing this label with i18n for multi-language support.
Integration with App
In the main App component (src/App.jsx:50-52), the ThemeSwitcher is positioned:
<div className="flex items-center gap-2">
<ThemeSwitcher />
<LangSwitcher />
</div>
It appears in the top-right corner alongside the language switcher.
CSS Requirements
For the transition effect to work, ensure your global CSS includes:
.theme-transition,
.theme-transition *,
.theme-transition *::before,
.theme-transition *::after {
transition: colors 500ms !important;
transition-delay: 0 !important;
}
localStorage Key
The component stores the theme preference using the key:
localStorage.setItem("theme", "dark"); // or "light"