How to Integrate `next-themes` for Theme Support in Next.js 14
Integrate next-themes by wrapping your Next.js 14 root layout with the ThemeProvider configured with attribute="class" and suppressHydrationWarning, then consume theme state using the useTheme hook in client components.
The woosal1337/blog repository demonstrates a production-ready pattern to integrate next-themes for theme support in Next.js 14 App Router applications. This implementation applies theme classes early during hydration to eliminate flash-of-unstyled-content (FOUC) and provides a type-safe API for toggling between light, dark, and system preferences.
Install the Dependency
Ensure next-themes is listed in your package.json (the repository uses version ^0.3.0):
"next-themes": "^0.3.0"
Install it via your package manager if it is not already present:
npm install next-themes@^0.3.0
Configure the ThemeProvider
Create a Provider Wrapper
Create a client component at components/providers/theme-provider.tsx that re-exports the next-themes provider. This isolates the "use client" directive, preventing your root layout from becoming a Client Component while preserving TypeScript types from next-themes/dist/types.
// components/providers/theme-provider.tsx
"use client";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import type { ThemeProviderProps } from "next-themes/dist/types";
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}
Update the Root Layout
In app/layout.tsx, import the wrapper and wrap your application content. Add suppressHydrationWarning to the <html> tag to prevent hydration mismatch warnings, as the attribute value is set client-side before React hydrates. Lines 108-115 in the source layout configure the provider with project-specific defaults:
// app/layout.tsx
import { ThemeProvider } from "@/components/providers/theme-provider";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html className="dark" suppressHydrationWarning>
<body>
<ThemeProvider
attribute="class"
defaultTheme="dark"
forcedTheme="dark"
enableSystem={false}
storageKey="chele.bi.theme"
disableTransitionOnChange
>
{children}
</ThemeProvider>
</body>
</html>
);
}
This configuration forces a dark theme (forcedTheme="dark"), disables system theme detection (enableSystem={false}), and persists the preference under the localStorage key chele.bi.theme. The disableTransitionOnChange prop suppresses CSS transitions during theme switches to prevent jarring visual effects.
Build a Theme Switcher UI
Access Theme State with useTheme
Any client component can access the current theme and setter function via the useTheme hook exported by next-themes.
"use client";
import { useTheme } from "next-themes";
export function ThemeDebug() {
const { theme, setTheme } = useTheme();
return <button onClick={() => setTheme("dark")}>Current: {theme}</button>;
}
Create the Switcher Component
The repository includes a complete implementation at components/ds/theme-switcher.tsx that renders a radio-group interface. It handles the client-side mounting nuance—components must wait for mount to avoid hydration mismatches where server HTML differs from the client's initial render.
// components/ds/theme-switcher.tsx
"use client";
import { cn } from "@/lib/utils";
import { useTheme } from "next-themes";
import * as React from "react";
const OPTIONS = [
{ value: "light", label: "Light" },
{ value: "dark", label: "Dark" },
{ value: "system", label: "Auto" },
] as const;
export function ThemeSwitcher({ className }: { className?: string }) {
const { theme, setTheme } = useTheme();
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => setMounted(true), []);
return (
<div role="radiogroup" aria-label="Theme" className={cn("inline-flex items-center gap-0.5 rounded-pill border p-0.5", className)}>
{OPTIONS.map(option => {
const selected = mounted && theme === option.value;
return (
<button
key={option.value}
type="button"
role="radio"
aria-checked={selected}
onClick={() => setTheme(option.value)}
className={cn(
"rounded-pill px-3 py-1 text-caption transition-colors duration-200",
selected ? "bg-action text-white" : "text-muted-foreground hover:text-foreground"
)}
>
{option.label}
</button>
);
})}
</div>
);
}
The mounted check on line 21 ensures the component only evaluates theme === option.value after React has mounted on the client. This prevents hydration errors while still calling setTheme immediately when the user clicks a button.
Summary
- Install
next-themes(version^0.3.0) as a project dependency according to thepackage.jsoninwoosal1337/blog. - Create a thin wrapper in
components/providers/theme-provider.tsxto isolate the"use client"directive and forward props tonext-themeswith full type safety. - Configure the provider in
app/layout.tsxwithsuppressHydrationWarningon the<html>element to prevent React hydration mismatches. - Set specific behavior using props like
forcedTheme,storageKey="chele.bi.theme", anddisableTransitionOnChangeto control persistence and UI transitions. - Consume the theme using the
useThemehook in client components, guarding against hydration mismatches by checkingmountedstate before rendering theme-dependent UI.
Frequently Asked Questions
How do I prevent hydration mismatches when using next-themes?
Add suppressHydrationWarning to the <html> tag in your root layout. For components that render UI elements differently based on the current theme (like active state indicators), initialize a mounted state variable to false and set it to true inside a useEffect hook. Only compare or display theme values after mounted is true to ensure server and client HTML align.
Can I force a specific theme and disable system detection?
Yes. Pass forcedTheme="dark" (or "light") and enableSystem={false} to the ThemeProvider props. As implemented in lines 108-115 of the woosal1337/blog source code at app/layout.tsx, this forces all users to view the site in dark mode regardless of their system preferences while still allowing the theme state to function internally.
Where should I store the theme preference?
Use the storageKey prop on the ThemeProvider to specify a custom localStorage key, such as storageKey="chele.bi.theme". This persists the user's selection across browser sessions. If omitted, next-themes defaults to the standard key, but explicit keys prevent collisions when running multiple apps on localhost.
Why wrap the NextThemesProvider in a separate component?
Creating a wrapper component in components/providers/theme-provider.tsx allows you to mark only that file as "use client" while keeping your root layout as a Server Component. This pattern maintains the performance benefits of React Server Components for the rest of your application while isolating client-side theme logic and avoiding "use client" pollution at the layout level.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →