Theme support is implemented via CSS custom properties + React Context or Tailwind dark mode. The goal is to toggle between light and dark themes without re-rendering the entire component tree unnecessarily.
How it works step-by-step:
localStorage.useEffect syncs the dark class on <html> with the theme state.useContext(ThemeContext) to read the theme.dark class.Option 1: Tailwind CSS with class-based dark mode:
1import { createContext, useContext, useState, useEffect, useMemo } from "react";23interface ThemeContextType {4 theme: "light" | "dark";5 toggleTheme: () => void;6 setTheme: (theme: "light" | "dark") => void;7}89const ThemeContext = createContext<ThemeContextType | undefined>(undefined);1011export function ThemeProvider({ children }: { children: React.ReactNode }) {12 const [theme, setTheme] = useState<"light" | "dark">(() => {13 if (typeof window !== "undefined") {14 const stored = localStorage.getItem("theme");15 if (stored === "light" || stored === "dark") return stored;16 return window.matchMedia("(prefers-color-scheme: dark)").matches17 ? "dark"18 : "light";19 }20 return "light";21 });2223 useEffect(() => {24 document.documentElement.classList.toggle("dark", theme === "dark");25 localStorage.setItem("theme", theme);26 }, [theme]);2728 const toggleTheme = () => setTheme(t => t === "light" ? "dark" : "light");2930 // Memoize to prevent unnecessary re-renders of consumers31 const value = useMemo(() => ({ theme, toggleTheme, setTheme }), [theme]);3233 return (34 <ThemeContext.Provider value={value}>35 {children}36 </ThemeContext.Provider>37 );38}3940export function useTheme() {41 const context = useContext(ThemeContext);42 if (!context) throw new Error("useTheme must be used within ThemeProvider");43 return context;44}4546// tailwind.config.ts47export default {48 darkMode: "class",49 theme: {50 extend: {51 colors: {52 surface: {53 light: "#ffffff",54 dark: "#1f2937"55 },56 text: {57 light: "#1f2937",58 dark: "#f9fafb"59 }60 }61 }62 }63};6465// Component using theme66function Card({ children }: { children: React.ReactNode }) {67 return (68 <div className="bg-surface-light dark:bg-surface-dark text-text-light dark:text-text-dark rounded-lg p-4 transition-colors">69 {children}70 </div>71 );72}
Option 2: CSS Custom Properties (runtime, no Tailwind):
1:root {2 --card-bg: #ffffff;3 --card-text: #1f2937;4 --card-border: #e5e7eb;5 --card-shadow: rgba(0, 0, 0, 0.1);6}78.dark {9 --card-bg: #1f2937;10 --card-text: #f9fafb;11 --card-border: #374151;12 --card-shadow: rgba(0, 0, 0, 0.3);13}1415.card {16 background: var(--card-bg);17 color: var(--card-text);18 border: 1px solid var(--card-border);19 box-shadow: 0 4px 6px var(--card-shadow);20 transition: background-color 0.2s, color 0.2s;21}
Option 3: System preference detection:
1function useSystemTheme() {2 useEffect(() => {3 const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");4 const handleChange = (e: MediaQueryListEvent) => {5 document.documentElement.classList.toggle("dark", e.matches);6 };7 mediaQuery.addEventListener("change", handleChange);8 return () => mediaQuery.removeEventListener("change", handleChange);9 }, []);10}
Configuration options:
darkMode: "class" — toggles via a class on <html> (recommended).darkMode: "media" — follows OS preference via prefers-color-scheme.darkMode: "selector" — custom selector for dark mode.Common pitfalls:
<script> in <head> that reads localStorage and adds the dark class before React hydrates.prefers-color-scheme: Users expect the app to follow their OS preference by default.useMemo to prevent unnecessary re-renders.#ffffff in component files.