Skip to main content
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

className
string
default:"''"
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

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)

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:
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
setTheme
(theme: Theme) => void
Function to update the theme preference
resolvedTheme
'light' | 'dark'
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

Source Reference

Source code: ~/workspace/source/src/contexts/ThemeContext.tsx:1-93

Complete Implementation

Here’s a full example implementing the theme system:
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:
  1. Light mode
  2. Dark mode
  3. System preference changes

Transition Smoothness

Add smooth transitions to theme-dependent styles:
body {
  transition: background-color 0.3s ease, color 0.3s 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:
  1. Wrapping your app correctly
  2. Not being unmounted and remounted
  3. localStorage is accessible in your environment

Navigation Components

Add ThemeToggle to your navbar

Components Overview

Explore other components

Build docs developers (and LLMs) love