Introduction
In Web applications, theming plays a crucial role in providing a personalised and visually appealing user experience. One common approach is to use a ThemeProvider
Context to manage themes across the application. There is no problem with application's which is completely client side. This blog is specific to developer who is looking for a solution where there application is combination of client and server components and theming depends on color-schema
or className
.
With the release of Next.js 14+, wrapping children in a Context will make children a client component. In this blog post, we'll explore how to implement a ThemeProvider which will help us to avoid wrapping complete application into a Context provider.
Implementing Theme Provider in Next.js 14+
To implement the Theme Provider without wrapping children in Context, we'll follow these steps:
Install Dependencies: Ensure you have the necessary dependencies installed, including
next-themes
for theme management andcookies-next
for cookies management.Create Theme Provider Component: Develop a custom
ThemeProvider
component that handles theme switching and cookie management.Integrate Theme Provider: Integrate the
ThemeProvider
component into your application layout to make sure client theming is synced with server on hydration.Use Theme Switcher : Integrate the
ThemeProvider
component into your application to switch theme.
Let's dive deeper into each step.
Step 1: Install Dependencies
First, make sure you have next-themes
installed in your Next.js project. If not, you can install it using npm or yarn:
npm install next-themes cookies-next
# or
yarn add next-themes cookies-next
Step 2: Create Theme Provider Component
In your project, create a custom ThemeProvider
component that leverages next-themes
for theme management. This component will handle theme switching and cookie management for server-side rendering.
// components/context/theme.tsx
import { setCookie } from "cookies-next";
import { ThemeProvider, useTheme } from "next-themes";
import type { ThemeProviderProps } from "next-themes/dist/types";
import { useEffect } from "react";
// Application theme provider
function AppThemeProvider({ children, ...props }: ThemeProviderProps) {
return (
<ThemeProvider enableColorScheme {...props}>
<AppThemeProviderHelper />
{children}
</ThemeProvider>
);
}
// Helper component to set theme in cookie
function AppThemeProviderHelper() {
const { theme } = useTheme();
useEffect(() => {
setCookie("__theme__", theme, {
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
path: "/",
});
}, [theme]);
return null;
}
export default AppThemeProvider;
Step 3: Integrate Theme Provider
Now, integrate the ThemeProvider
component into your application layout in your app/layout.tsx
file or any child layout as per requirement:
import { MonaSans_Font } from "@/assets/fonts/MonaSans";
import dynamic from "next/dynamic";
const AppThemeProvider = dynamic(() => import("@/components/context/theme"), {
ssr: false,
});
export default async function AppLayout({
children,
}: {
children: React.ReactNode;
}) {
const theme = cookies().get("__theme__")?.value || "system";
return (
<html
className={theme}
lang="en"
style={theme !== "system" ? { colorScheme: theme } : {}}
>
<body className={`${MonaSans_Font.className} flex flex-col h-full w-full overflow-hidden`}>
{children}
<AppThemeProvider
attribute="class"
defaultTheme={theme}
enableSystem
/>
</body>
</html>
);
}
Step 4: Integrate Theme Provider in Theme Switching
A Tab's based switcher can be created as below to switch theme Using AppThemeProvider
.
"use client";
import AppThemeProvider from "@/components/context/theme";
import { Tabs, TabsList, TabsTrigger } from "ui/components/tabs";
import { useTheme } from "next-themes";
function Tab() {
const { setTheme, theme } = useTheme();
return (
<Tabs className="w-full" onValueChange={setTheme} value={theme}>
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="light">Light</TabsTrigger>
<TabsTrigger value="dark">Dark</TabsTrigger>
<TabsTrigger value="system">System</TabsTrigger>
</TabsList>
</Tabs>
);
}
function ThemeTabs() {
return (
<AppThemeProvider attribute="class" defaultTheme="system" enableSystem>
<Tab />
</AppThemeProvider>
);
}
export default ThemeTabs;
Conclusion
Implementing a Theme Provider in Next.js 14+ without wrapping children in Context offers a cleaner and more efficient way to manage themes in your application without making complete application a client component.
Top comments (8)
thank you so much for this, works fine with nextJS 14.2.11. i did want to point out in app/layout.tsx,
AppThemeProvider
needs to accept achildren
prop. I ended up wrapping the theme switching component insideAppThemeProvider
in app/layout.tsx.@yuens1002 Thanks for your kind word.
if your are using tailwind css, you can skip passing children to to AppThemeProvider from
layout.tsx
. Because while initial hydration you don't need to know about the theme because you are already passing theme class intohtml
tag's class attribute which will select dark or light theme for user without flicker.Also, if you want to access the theme for switching or showing on the ui which theme is selected. You can just wrap that part of ui inside
AppThemeProvider
in client components. Whole purpose here is to not wrap application inside context in layout.tsx and tailwindcss.Now if it is completely necessary for you to wrap your children into AppThemeProvider then don't use this method instead use template.js/jsx/tsx file and make that a child component and inside that Wrap AppThemeProvider around your children.
Thank you.
looking at this...
// Application theme provider
function AppThemeProvider({children, ...props }: ThemeProviderProps) {
return (
<ThemeProvider enableColorScheme {...props}>
<AppThemeProviderHelper />
{children}
</ThemeProvider>
);
}
and this in your app/layout.tsx where
AppThemeProvider
is being called without a children<body className={
${MonaSans_Font.className} flex flex-col h-full w-full overflow-hidden`}>{children}
this will cause a type error as it is being called without the required children prop as it is typed as a requirement in the
AppTHemeProvider
function signature.Ok, sorry for that. I think it shouldn't however if i m wrong you can always pass null to avoid those error. In react null is valid children.
Thanks
I like this! Thanks 🙌
can you tell more about tabs components in the last code? I tried to use select component. I got an error with theme.tsx as shown in first code :
I might not be able to help without full context or code demo.
This may help: if you have forgot to add
"use client";
in first line ofstep 3
file, that's why it might be throwing you error. The main purpose of this approach was to use client component only for the components which is toggling the theme that way you can keep your app server rendered while rendering theme switch in client side dynamically.uh.. which step 3? (there are 2 step 3s ... I guess)
if you mean when integrating ThemeProvider in app/layout.tsx, is it possible to add "use client" in RootLayout?
And If you mean in the last step I already done that.