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:
- Flash of unstyled content (FOUC): if the theme is only resolved on the client (from
localStorageor the system preference), the server renders the default theme and the page visibly flips once the client corrects it - 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 :
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, but the theme choice lives only on the client. On a hard refresh the server has no idea what the visitor picked, so it renders the default theme and the page flashes when next-themes kicks in.
The new way
This is the setup this blog runs on today (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 no longer creates any provider of its own; it simply 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 default theme, so the very first server render already has the correct theme and there is no flash on load. 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 ensures the correct theme is applied on the very first render
- No FOUC: prevents the flash of the incorrect theme on initial load
- 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