Skip to main content

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 useTheme hook

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

Decision Makers

  • Development Team
  • UI/UX Team