AniDojo’s theme system provides dark/light mode switching with system preference detection and persistent storage.
ThemeToggle
A button component that toggles between light and dark themes using the theme context.
Import
import ThemeToggle from '@/components/ThemeToggle' ;
Props
Optional CSS classes to apply to the button element
Features
Animated Icons : Sun icon for dark mode, Moon icon for light mode
Hydration Safe : Prevents hydration mismatches with mounting check
Accessible : Includes aria-label for screen readers
Smooth Transitions : Color transitions on hover and theme change
Usage
Navbar Integration
Custom Styling
Settings Page
import ThemeToggle from '@/components/ThemeToggle' ;
export default function Navbar () {
return (
< nav >
< div className = "flex items-center gap-4" >
< ThemeToggle />
{ /* Other nav items */ }
</ div >
</ nav >
);
}
Hydration Safety
The component uses a mounting state to prevent hydration mismatches:
const [ mounted , setMounted ] = useState ( false );
useEffect (() => {
setMounted ( true );
}, []);
if ( ! mounted ) {
return < div className = { `w-9 h-9 ${ className } ` } /> ;
}
This ensures the server-rendered HTML matches the initial client render before showing the theme-dependent icon.
Icon States
< Sun className = "w-5 h-5 text-yellow-400" />
Shows sun icon when in dark mode (clicking switches to light) < Moon className = "w-5 h-5 text-blue-400" />
Shows moon icon when in light mode (clicking switches to dark)
Source Reference
Source code: ~/workspace/source/src/components/ThemeToggle.tsx:1-34
ThemeContext
React context providing theme state and controls throughout the application.
Import
import { ThemeProvider , useTheme } from '@/contexts/ThemeContext' ;
Provider Setup
Wrap your app with ThemeProvider at the root level:
app/layout.tsx
pages/_app.tsx
import { ThemeProvider } from '@/contexts/ThemeContext' ;
export default function RootLayout ({ children }) {
return (
< html >
< body >
< ThemeProvider >
{ children }
</ ThemeProvider >
</ body >
</ html >
);
}
useTheme Hook
Access theme state and controls in any component:
import { useTheme } from '@/contexts/ThemeContext' ;
export default function MyComponent () {
const { theme , setTheme , resolvedTheme } = useTheme ();
return (
< div >
< p > Current theme: { theme } </ p >
< p > Resolved theme: { resolvedTheme } </ p >
< button onClick = { () => setTheme ( 'dark' ) } > Dark </ button >
< button onClick = { () => setTheme ( 'light' ) } > Light </ button >
< button onClick = { () => setTheme ( 'system' ) } > System </ button >
</ div >
);
}
Type Definition
type Theme = 'light' | 'dark' | 'system' ;
interface ThemeContextType {
theme : Theme ; // Current theme setting
setTheme : ( theme : Theme ) => void ; // Update theme
resolvedTheme : 'light' | 'dark' ; // Actual theme in use
}
Return Values
theme
'light' | 'dark' | 'system'
The user’s theme preference setting
Function to update the theme preference
The actual theme being applied (resolves ‘system’ to ‘light’ or ‘dark’)
System Theme Detection
The context automatically detects system theme preferences:
const mediaQuery = window . matchMedia ( '(prefers-color-scheme: dark)' );
const handleChange = () => {
if ( theme === 'system' ) {
setResolvedTheme ( mediaQuery . matches ? 'dark' : 'light' );
}
};
mediaQuery . addEventListener ( 'change' , handleChange );
When theme is ‘system’ :
resolvedTheme automatically updates based on OS preference
Listens for system theme changes in real-time
Persistence
Theme preference is saved to localStorage:
anidojo_theme
'light' | 'dark' | 'system'
localStorage key storing the user’s theme preference
// Save theme
localStorage . setItem ( 'anidojo_theme' , theme );
// Load theme on mount
const savedTheme = localStorage . getItem ( 'anidojo_theme' );
CSS Classes Applied
The context applies these classes to document.documentElement:
// Applied classes
< html class = "dark-theme dark" >
dark-theme - Custom AniDojo dark theme class
dark - Tailwind CSS dark mode class
// Applied classes
< html class = "light-theme" >
light-theme - Custom AniDojo light theme class
Source Reference
Source code: ~/workspace/source/src/contexts/ThemeContext.tsx:1-93
Complete Implementation
Here’s a full example implementing the theme system:
app/layout.tsx
components/CustomThemeSelector.tsx
components/ThemeAwareComponent.tsx
import { ThemeProvider } from '@/contexts/ThemeContext' ;
import Navbar from '@/components/Navbar' ;
import ThemeToggle from '@/components/ThemeToggle' ;
export default function RootLayout ({ children }) {
return (
< html >
< body >
< ThemeProvider >
< header className = "flex items-center justify-between p-4" >
< div > AniDojo </ div >
< ThemeToggle />
</ header >
< main > { children } </ main >
</ ThemeProvider >
</ body >
</ html >
);
}
Styling with Theme
Tailwind Dark Mode
Use Tailwind’s dark: modifier with the theme system:
< div className = "bg-white text-black dark:bg-gray-900 dark:text-white" >
Content adapts to theme
</ div >
Custom CSS
Target theme classes in your CSS:
/* Light theme styles */
.light-theme {
--background : #ffffff ;
--foreground : #000000 ;
}
/* Dark theme styles */
.dark-theme {
--background : #1a1a1a ;
--foreground : #ffffff ;
}
/* Use CSS variables */
.card {
background : var ( --background );
color : var ( --foreground );
}
Conditional Rendering
import { useTheme } from '@/contexts/ThemeContext' ;
export default function Hero () {
const { resolvedTheme } = useTheme ();
return (
< div >
{ resolvedTheme === 'dark' ? (
< img src = "/hero-dark.jpg" alt = "Hero" />
) : (
< img src = "/hero-light.jpg" alt = "Hero" />
) }
</ div >
);
}
Best Practices
Provider Placement
Place ThemeProvider as high as possible in your component tree:
< html >
< body >
< ThemeProvider > { /* ✓ Wrap entire app */ }
< App />
</ ThemeProvider >
</ body >
</ html >
Error Handling
The useTheme hook throws an error if used outside the provider:
export function useTheme () {
const context = useContext ( ThemeContext );
if ( context === undefined ) {
throw new Error ( 'useTheme must be used within a ThemeProvider' );
}
return context ;
}
Always ensure components using useTheme are wrapped in ThemeProvider.
Testing Themes
Test your components in all theme modes:
Light mode
Dark mode
System preference changes
Transition Smoothness
Add smooth transitions to theme-dependent styles:
body {
transition : background-color 0.3 s ease , color 0.3 s ease ;
}
Common Patterns
Theme-Aware Components
import { useTheme } from '@/contexts/ThemeContext' ;
export default function Card ({ children }) {
const { resolvedTheme } = useTheme ();
const bgColor = resolvedTheme === 'dark' ? 'bg-gray-800' : 'bg-white' ;
return (
< div className = { ` ${ bgColor } rounded-lg shadow p-4` } >
{ children }
</ div >
);
}
Settings Panel
import { useTheme } from '@/contexts/ThemeContext' ;
import ThemeToggle from '@/components/ThemeToggle' ;
export default function SettingsPanel () {
const { theme } = useTheme ();
return (
< div className = "settings" >
< h2 > Appearance </ h2 >
< div className = "setting-row" >
< div >
< h3 > Theme </ h3 >
< p > Current: { theme } </ p >
</ div >
< ThemeToggle />
</ div >
</ div >
);
}
Troubleshooting
Hydration Mismatch
If you see hydration errors, ensure you’re using the mounting check:
const [ mounted , setMounted ] = useState ( false );
useEffect (() => {
setMounted ( true );
}, []);
if ( ! mounted ) {
return < Skeleton /> ; // Return placeholder during SSR
}
// Render theme-dependent content
localStorage Undefined
Handle SSR environments where localStorage isn’t available:
if ( typeof window !== 'undefined' ) {
const savedTheme = localStorage . getItem ( 'anidojo_theme' );
// Use savedTheme
}
Theme Not Persisting
Check that the ThemeProvider is:
Wrapping your app correctly
Not being unmounted and remounted
localStorage is accessible in your environment
Navigation Components Add ThemeToggle to your navbar
Components Overview Explore other components