# Theming Architecture ## Overview A robust theming system enables applications to support multiple visual appearances (light/dark modes, brand themes) while maintaining consistency and developer experience. ## CSS Custom Properties Architecture ### Base Setup ```css /* 1. Define the token contract */ :root { /* Color scheme */ color-scheme: light dark; /* Base tokens that don't change */ --font-sans: Inter, system-ui, sans-serif; --font-mono: "JetBrains Mono", monospace; /* Animation tokens */ --duration-fast: 150ms; --duration-normal: 250ms; --duration-slow: 400ms; --ease-default: cubic-bezier(0.4, 0, 0.2, 1); /* Z-index scale */ --z-dropdown: 100; --z-sticky: 200; --z-modal: 300; --z-popover: 400; --z-tooltip: 500; } /* 2. Light theme (default) */ :root, [data-theme="light"] { --color-bg: #ffffff; --color-bg-subtle: #f8fafc; --color-bg-muted: #f1f5f9; --color-bg-emphasis: #0f172a; --color-text: #0f172a; --color-text-muted: #475569; --color-text-subtle: #94a3b8; --color-border: #e2e8f0; --color-border-muted: #f1f5f9; --color-accent: #3b82f6; --color-accent-hover: #2563eb; --color-accent-muted: #dbeafe; --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1); --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1); } /* 3. Dark theme */ [data-theme="dark"] { --color-bg: #0f172a; --color-bg-subtle: #1e293b; --color-bg-muted: #334155; --color-bg-emphasis: #f8fafc; --color-text: #f8fafc; --color-text-muted: #94a3b8; --color-text-subtle: #64748b; --color-border: #334155; --color-border-muted: #1e293b; --color-accent: #60a5fa; --color-accent-hover: #93c5fd; --color-accent-muted: #1e3a5f; --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3); --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.4); --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.5); } /* 4. System preference detection */ @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { /* Inherit dark theme values */ --color-bg: #0f172a; /* ... other dark values */ } } ``` ### Using Tokens in Components ```css .card { background: var(--color-bg-subtle); border: 1px solid var(--color-border); border-radius: 0.5rem; box-shadow: var(--shadow-sm); padding: 1.5rem; } .card-title { color: var(--color-text); font-family: var(--font-sans); font-size: 1.125rem; font-weight: 600; } .card-description { color: var(--color-text-muted); margin-top: 0.5rem; } .button-primary { background: var(--color-accent); color: white; transition: background var(--duration-fast) var(--ease-default); } .button-primary:hover { background: var(--color-accent-hover); } ``` ## React Theme Provider ### Complete Implementation ```tsx // theme-provider.tsx import * as React from "react"; type Theme = "light" | "dark" | "system"; type ResolvedTheme = "light" | "dark"; interface ThemeProviderProps { children: React.ReactNode; defaultTheme?: Theme; storageKey?: string; attribute?: "class" | "data-theme"; enableSystem?: boolean; disableTransitionOnChange?: boolean; } interface ThemeProviderState { theme: Theme; resolvedTheme: ResolvedTheme; setTheme: (theme: Theme) => void; toggleTheme: () => void; } const ThemeProviderContext = React.createContext< ThemeProviderState | undefined >(undefined); export function ThemeProvider({ children, defaultTheme = "system", storageKey = "theme", attribute = "data-theme", enableSystem = true, disableTransitionOnChange = false, }: ThemeProviderProps) { const [theme, setThemeState] = React.useState(() => { if (typeof window === "undefined") return defaultTheme; return (localStorage.getItem(storageKey) as Theme) || defaultTheme; }); const [resolvedTheme, setResolvedTheme] = React.useState("light"); // Get system preference const getSystemTheme = React.useCallback((): ResolvedTheme => { if (typeof window === "undefined") return "light"; return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; }, []); // Apply theme to DOM const applyTheme = React.useCallback( (newTheme: ResolvedTheme) => { const root = document.documentElement; // Disable transitions temporarily if (disableTransitionOnChange) { const css = document.createElement("style"); css.appendChild( document.createTextNode( `*,*::before,*::after{transition:none!important}`, ), ); document.head.appendChild(css); // Force repaint (() => window.getComputedStyle(document.body))(); // Remove after a tick setTimeout(() => { document.head.removeChild(css); }, 1); } // Apply attribute if (attribute === "class") { root.classList.remove("light", "dark"); root.classList.add(newTheme); } else { root.setAttribute(attribute, newTheme); } // Update color-scheme for native elements root.style.colorScheme = newTheme; setResolvedTheme(newTheme); }, [attribute, disableTransitionOnChange], ); // Handle theme changes React.useEffect(() => { const resolved = theme === "system" ? getSystemTheme() : theme; applyTheme(resolved); }, [theme, applyTheme, getSystemTheme]); // Listen for system theme changes React.useEffect(() => { if (!enableSystem || theme !== "system") return; const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); const handleChange = () => { applyTheme(getSystemTheme()); }; mediaQuery.addEventListener("change", handleChange); return () => mediaQuery.removeEventListener("change", handleChange); }, [theme, enableSystem, applyTheme, getSystemTheme]); // Persist to localStorage const setTheme = React.useCallback( (newTheme: Theme) => { localStorage.setItem(storageKey, newTheme); setThemeState(newTheme); }, [storageKey], ); const toggleTheme = React.useCallback(() => { setTheme(resolvedTheme === "light" ? "dark" : "light"); }, [resolvedTheme, setTheme]); const value = React.useMemo( () => ({ theme, resolvedTheme, setTheme, toggleTheme, }), [theme, resolvedTheme, setTheme, toggleTheme], ); return ( {children} ); } export function useTheme() { const context = React.useContext(ThemeProviderContext); if (context === undefined) { throw new Error("useTheme must be used within a ThemeProvider"); } return context; } ``` ### Theme Toggle Component ```tsx // theme-toggle.tsx import { Moon, Sun, Monitor } from "lucide-react"; import { useTheme } from "./theme-provider"; export function ThemeToggle() { const { theme, setTheme } = useTheme(); return (
); } ``` ## Multi-Brand Theming ### Brand Token Structure ```css /* Brand A - Corporate Blue */ [data-brand="corporate"] { --brand-primary: #0066cc; --brand-primary-hover: #0052a3; --brand-secondary: #f0f7ff; --brand-accent: #00a3e0; --brand-font-heading: "Helvetica Neue", sans-serif; --brand-font-body: "Open Sans", sans-serif; --brand-radius: 0.25rem; --brand-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } /* Brand B - Modern Startup */ [data-brand="startup"] { --brand-primary: #7c3aed; --brand-primary-hover: #6d28d9; --brand-secondary: #faf5ff; --brand-accent: #f472b6; --brand-font-heading: "Poppins", sans-serif; --brand-font-body: "Inter", sans-serif; --brand-radius: 1rem; --brand-shadow: 0 4px 12px rgba(124, 58, 237, 0.15); } /* Brand C - Minimal */ [data-brand="minimal"] { --brand-primary: #171717; --brand-primary-hover: #404040; --brand-secondary: #fafafa; --brand-accent: #171717; --brand-font-heading: "Space Grotesk", sans-serif; --brand-font-body: "IBM Plex Sans", sans-serif; --brand-radius: 0; --brand-shadow: none; } ``` ## Accessibility Considerations ### Reduced Motion ```css @media (prefers-reduced-motion: reduce) { :root { --duration-fast: 0ms; --duration-normal: 0ms; --duration-slow: 0ms; } *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; } } ``` ### High Contrast Mode ```css @media (prefers-contrast: high) { :root { --color-text: #000000; --color-text-muted: #000000; --color-bg: #ffffff; --color-border: #000000; --color-accent: #0000ee; } [data-theme="dark"] { --color-text: #ffffff; --color-text-muted: #ffffff; --color-bg: #000000; --color-border: #ffffff; --color-accent: #ffff00; } } ``` ### Forced Colors ```css @media (forced-colors: active) { .button { border: 2px solid currentColor; } .card { border: 1px solid CanvasText; } .link { text-decoration: underline; } } ``` ## Server-Side Rendering ### Preventing Flash of Unstyled Content ```tsx // Inline script to prevent FOUC const themeScript = ` (function() { const theme = localStorage.getItem('theme') || 'system'; const isDark = theme === 'dark' || (theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches); document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light'); document.documentElement.style.colorScheme = isDark ? 'dark' : 'light'; })(); `; // In Next.js layout - note: inline scripts should be properly sanitized in production export default function RootLayout({ children }) { return (