Theme Switcher in Next.js
Cookie-persisted theming that keeps server components on the server
Before getting into the implementation, one misconception is worth clearing up because I believed it for a while: wrapping children in a client context provider does not turn those children into client components. When children are passed as props through a client component, they stay server components; this is the composition pattern Next.js has documented since version 13. So the reason to rethink the classic "wrap the app in a ThemeProvider" setup is not that it drags your whole tree onto the client.
The real problems are more practical:
- The server is blind: if the theme is only resolved on the client (from
localStorageor the system preference), the HTML the server sends assumes the default theme and only the client's inline script corrects it. There is no visible flip, becausenext-themesruns that script before paint, but the server has still rendered the wrong thing - Server-side rendering: for the server to render the right theme on first paint, the visitor's choice has to live somewhere the server can read. That means a cookie, not
localStorage - Hydration mismatches: any component that renders differently per theme needs guarding, or React will complain that server and client markup disagree
Generally speaking, implementing a theme switcher involves three main steps:
- Create Theme Provider Component: Develop a custom
ThemeProvidercomponent that handles theme switching and cookie managementThis will be needed to persist theme choice - Use Theme Switcher: Integrate a
ThemeSwitchercomponent into your application to toggle the theme - Integrate Theme Provider: Wire the
ThemeProviderinto your application layout so the client theme is synced with the server on hydration
The old way
1. Create Theme Provider Component
In a typical React application, you would wrap the entire application in a ThemeProvider context provider backed by next-themes:
2. Use Theme Switcher
To accompany the ThemeProvider, you would need a ThemeSwitcher component to toggle between themes. You can already see the reliance on the useTheme hook from the next-themes package to get the current theme and toggle it.
3. Integrate Theme Provider
What happens now is that in our application we will need to wrap all of our content with the ThemeProvider as shown below.
This works, and there is no visible flash either: next-themes injects a small blocking inline script that reads localStorage and sets the class on <html> before the page paints. The problem is the server. On a hard refresh it has no idea what the visitor picked, so the HTML it sends, and anything rendered on the server from the theme, assumes the default theme and only the client-side script corrects it.
The new way
This is the setup this blog ran on when I wrote this (Next.js 15). The provider still wraps the layout, but two things change: the theme choice is persisted to a cookie so the server can read it, and the layout is a server component that resolves the theme before rendering. The children it wraps remain server components.
1. Create Theme Provider Component
The provider is a thin wrapper around next-themes with a helper component that writes the current theme to a cookie whenever it changes. This makes sure the choice is persisted across sessions and, more importantly, is visible to the server:
2. Use Theme Switcher
The ThemeSwitcher barely changes: it still just consumes the shared context from the app-level ThemeProvider above. We still need a mounted-state check to prevent hydration mismatches, since the button's icon depends on the resolved theme:
This sample is simplified for the post; it uses react-icons for brevity, while the production component in my design system renders its own Icon set.
3. Integrate Theme Provider
The layout container is a server component. It reads the theme cookie with await cookies() (cookies are async in Next.js 15) and passes the value as the provider's defaultTheme. To be precise about what that buys: with attribute='class' the class on <html> is still applied by the inline script next-themes injects before paint; the cookie seeds the default so the server starts from the visitor's choice instead of the site default, and server output and script agree from the first render. The provider is dynamically imported and wraps the children; since they are passed through as props, they stay server components:
Key benefits
This approach provides several advantages:
- Server Components Stay Server Components: children passed through the client provider are not converted to client components; only the provider itself ships to the client
- Server-Side Theme Detection: reading the cookie on the server lets the server-rendered HTML and the
defaultThemehanded tonext-themesstart from the visitor's choice rather than the site default, so server output and the inline script agree from the first render - No visible flash, with or without the cookie: the inline script
next-themesinjects applies the stored theme before the page paints; the cookie's job is to keep the server honest, not to hide a flicker - Persistent Theme Selection: the choice persists across sessions via the cookie
- Hydration Safety: the mounted-state check in the switcher prevents hydration mismatches
Common gotchas
When implementing this pattern, watch out for:
- Cookie Reading: make sure to use
await cookies()in Next.js 15+, as the cookies API is now async - Dynamic Import: don't set
ssr: falseon theThemeProviderdynamic import. We want SSR, just with the correct theme from the cookie - Hydration Mismatches: always check mounted state before rendering theme-dependent content in your theme switcher
- Default Theme: have a sensible fallback theme in case the cookie doesn't exist