Source:
ocean/docs/adr/0010-theme-management-with-next-themes.md| ✏️ Edit on GitHub
ADR-0010: Theme Management with next-themes
Date: 2025-07-29
Status
Accepted
Context
Our application requires a robust theme management system that:
- Supports light/dark mode switching
- Respects user system preferences
- Prevents flash of incorrect theme on page load (FOUC - Flash of Unstyled Content)
- Works seamlessly with our TanStack Router setup
- Integrates well with our Figma-based design token workflow
- Is compatible with server-side rendering (SSR) if we add it in the future
We discovered we had duplicate theme providers in our codebase - a custom implementation and next-themes, which was causing conflicts and making theme management unnecessarily complex.
Decision
We have decided to use next-themes as our sole theme management solution, removing our custom theme provider implementation.
Despite its name suggesting Next.js specificity, next-themes is a general-purpose React theme management library that works with any React application, including those using Vite and TanStack Router.
Consequences
Positive
- No Theme Flash: next-themes prevents FOUC by injecting a small script before React hydrates
- System Preference Support: Automatically detects and respects user's OS theme preference
- LocalStorage Persistence: User's theme choice persists across sessions
- SSR Ready: If we add SSR in the future, next-themes handles it correctly
- Industry Standard: Most widely adopted theme solution in the React ecosystem
- Small Bundle Size: Only ~2.5KB gzipped
- TypeScript Support: Fully typed for better developer experience
- Simple API: Clean and intuitive API with
useThemehook
Negative
- External Dependency: Adds another dependency to maintain
- Name Confusion: The "next" prefix might confuse developers who don't realize it works outside Next.js
- Migration Effort: Required removing custom theme provider code (though this simplified our codebase)
Implementation Details
Basic Setup
// src/routes/__root.tsx
import { ThemeProvider } from 'next-themes'
export const Route = createRootRoute({
component: () => (
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<App />
</ThemeProvider>
),
})
Usage in Components
import { useTheme } from 'next-themes'
function ThemeToggle() {
const { theme, setTheme } = useTheme()
return <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>Toggle theme</button>
}
CSS Variables Integration
Our theme tokens in /src/styles/tokens.css:
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
/* ... other light theme variables */
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
/* ... other dark theme variables */
}
}
Tailwind Integration
Using CSS variables with Tailwind classes:
<div className="bg-background text-foreground">Theme-aware content</div>
Alternatives Considered
1. Custom Theme Provider
- Pros: Full control, no external dependencies
- Cons: Need to handle FOUC, SSR, system preferences ourselves
- Rejected because: Reinventing a well-solved problem
2. Tailwind Dark Mode Classes
- Pros: No JavaScript required
- Cons: No system preference detection, requires class manipulation
- Rejected because: Less flexible, no runtime control
3. CSS-only with prefers-color-scheme
- Pros: No JavaScript, respects system preference
- Cons: No user override option, limited control
- Rejected because: Users can't override system preference
4. React Context Only
- Pros: Simple, no dependencies
- Cons: Flash of wrong theme, no persistence
- Rejected because: Poor user experience with theme flashing
References
- next-themes Documentation
- Preventing Theme Flashing
- Design System Workflow Documentation
- Figma Design System Integration
Decision Makers
- Development Team
- UI/UX Team