Next.js' class persistence in <html > element
Unanswered
Ocicat posted this in #help-forum
OcicatOP
Hi everyone,
I'm running a Next.js 14.1 project. In my root layout.tsx, which is supposed to wrap every other page, I am applying classes to the <html> element. One class is fixed, and another is dynamic, meaning I use a button to toggle class "dark" , to implement a theme switch with Tailwind CSS.
The switch works fine on the landing page, but when I navigate to another page, such as "register," the "dark" class vanishes and the theme defaults back to light. The fixed class is still there, somehow, but the dynamic one is gone.
If the <html> element wraps everything, why isn't the "dark" class persisting between pages? This is puzzling because the fixed class remains.
Is this issue related to the dynamic class being applied client-side?
How can I resolve this problem and ensure the class persists between different pages?
I do not want to use the Context API. I am storing the theme in localStorage, but I shouldn't have to resort to it. The class of <html> should hold the state. The theme should be applied site-wide and that's why I had chosen the root <html > element.
Any help would be greatly appreciated.
Thanks in advance!
I'm running a Next.js 14.1 project. In my root layout.tsx, which is supposed to wrap every other page, I am applying classes to the <html> element. One class is fixed, and another is dynamic, meaning I use a button to toggle class "dark" , to implement a theme switch with Tailwind CSS.
The switch works fine on the landing page, but when I navigate to another page, such as "register," the "dark" class vanishes and the theme defaults back to light. The fixed class is still there, somehow, but the dynamic one is gone.
If the <html> element wraps everything, why isn't the "dark" class persisting between pages? This is puzzling because the fixed class remains.
Is this issue related to the dynamic class being applied client-side?
How can I resolve this problem and ensure the class persists between different pages?
I do not want to use the Context API. I am storing the theme in localStorage, but I shouldn't have to resort to it. The class of <html> should hold the state. The theme should be applied site-wide and that's why I had chosen the root <html > element.
Any help would be greatly appreciated.
Thanks in advance!
28 Replies
Well seems like you are loosing state on navigation
Not sure why though.
OcicatOP
Because that turns everything into a giant client component. I have implemented the theme via the Context API before. I want to do it differently.
My research tells me that the lack of dynamic class persistence between pages is a deliberate behavior by React/Next.js.
So what I have done is extract the theme syncing and applying to a custom hook that I call on every page.
Hopefully, it should work.
I will let you know
My research tells me that the lack of dynamic class persistence between pages is a deliberate behavior by React/Next.js.
So what I have done is extract the theme syncing and applying to a custom hook that I call on every page.
Hopefully, it should work.
I will let you know
Are you using
<Link>?@Ocicat Because that turns everything into a giant client component. I have implemented the theme via the Context API before. I want to do it differently.
My research tells me that the lack of dynamic class persistence between pages is a deliberate behavior by React/Next.js.
So what I have done is extract the theme syncing and applying to a custom hook that I call on every page.
Hopefully, it should work.
I will let you know
No, Context will NOT turn your app into client component
OcicatOP
Doesn't it? You have to wrap eveything in a Context provider in the layout.tsx
Yes, I'm using Link
If you pass a server component inside a client component as children, it wont turn the children into client components
OcicatOP
The thin is , simply adding/removing class "dark" to the root < html > element is way simpler than setting up Context API just for that. It seems way overkill to me
But I have to admit that I presumed classList would persist between pages in a SPA.
Well i do not know why you are loosing state. I currently do not remember if a normal variable keeps its value on navigation but i presume not.
Are you using useState?
Are you using useState?
Also a suggestion i would give you is to try out "next-themes" if you want to apply theme switching and stuff.
OcicatOP
So, this project uses Tailwind CSS, where classes 'dark: ...' are applied if a parent element has the class 'dark'. Therefore, simply applying/removing 'dark' to/from <html> will enforce app-wide theme.
I have a theme switch ('light', 'dark', and 'system') that also remembers the last value the user chose by getting it from localStorage.
The theme switch uses useState, useEffect, and useLayoutEffect
I have a theme switch ('light', 'dark', and 'system') that also remembers the last value the user chose by getting it from localStorage.
The theme switch uses useState, useEffect, and useLayoutEffect
OcicatOP
@Clown , my nsolution of exporting the yseEffects and imorting them in each page is working !
Copilot had this to say, though:
"Certainly! The issue you’re encountering with the dynamic class not persisting between pages in your Next.js app using Tailwind CSS is a common one. Let’s delve into the reasons behind this behavior and explore potential solutions.
Dynamic Classes and Rendering Order:
When you dynamically add or remove classes in your Next.js app, the rendering order matters.
Tailwind CSS generates styles based on the classes present in your HTML markup during the build process.
However, dynamic classes are applied after rendering, which means that their effects might not be captured by Tailwind unless there’s another element with the same class as a static class.
The Safelist Solution:
To address this issue, you can use Tailwind CSS’s safelist feature.
In your tailwind.config.js, define a safelist array containing all the Tailwind classes that you need to be generated, even if they don’t exist as static classes in your code.
Here’s an example of how to set up the safelist:
JavaScript
// tailwind.config.js
module.exports = {
content: [
'./pages//*.{html,js}',
'./components//*.{html,js}',
],
safelist: [
'from-red-500',
'from-orange-500',
// Add other classes you're using dynamically
],
// Other Tailwind config options...
};
By including these classes in the safelist, Tailwind will always generate them, ensuring that dynamically applied classes take effect.
Router Dependency in useEffect:
Make sure to include the router (from Next.js) in the dependency array of your useEffect.
This ensures that the effect runs whenever the router changes, allowing the dynamic class to be updated correctly.
Full Class Names:
When dynamically applying classes, use full Tailwind CSS class names in your code.
Avoid using partial names along with dynamic values.
Generate the complete class name dynamically and apply it directly.
Copilot had this to say, though:
"Certainly! The issue you’re encountering with the dynamic class not persisting between pages in your Next.js app using Tailwind CSS is a common one. Let’s delve into the reasons behind this behavior and explore potential solutions.
Dynamic Classes and Rendering Order:
When you dynamically add or remove classes in your Next.js app, the rendering order matters.
Tailwind CSS generates styles based on the classes present in your HTML markup during the build process.
However, dynamic classes are applied after rendering, which means that their effects might not be captured by Tailwind unless there’s another element with the same class as a static class.
The Safelist Solution:
To address this issue, you can use Tailwind CSS’s safelist feature.
In your tailwind.config.js, define a safelist array containing all the Tailwind classes that you need to be generated, even if they don’t exist as static classes in your code.
Here’s an example of how to set up the safelist:
JavaScript
// tailwind.config.js
module.exports = {
content: [
'./pages//*.{html,js}',
'./components//*.{html,js}',
],
safelist: [
'from-red-500',
'from-orange-500',
// Add other classes you're using dynamically
],
// Other Tailwind config options...
};
By including these classes in the safelist, Tailwind will always generate them, ensuring that dynamically applied classes take effect.
Router Dependency in useEffect:
Make sure to include the router (from Next.js) in the dependency array of your useEffect.
This ensures that the effect runs whenever the router changes, allowing the dynamic class to be updated correctly.
Full Class Names:
When dynamically applying classes, use full Tailwind CSS class names in your code.
Avoid using partial names along with dynamic values.
Generate the complete class name dynamically and apply it directly.
make a file named providers.tsx
and then put this inside
Then, wrap your layout with
and then put this inside
"use client";
import { ThemeProvider } from "next-themes";
export function Themes({ children }: { children: React.ReactNode }) {
return <ThemeProvider attribute="class">{children}</ThemeProvider>;
}Then, wrap your layout with
<Themes> instead of <ThemeProvider>I use this with tailwind css
and have no problems
It's super easy
and then in your tailwind config, add
darkMode: ["class"]@Ocicat if you found a solution right click the answer, go to apps -> mark as solution
OcicatOP
@!=tgt , but it's not the solution advocated by some, it's the solution to, on load, fetch the theme from lcoalstorage and sync accordingly
OcicatOP
Next themes, Context API , etc
OcicatOP
@!=tgt , I have found an even simpler solution!
After watching this video: https://www.youtube.com/watch?v=guh9qzxkb1o
I decided to try a solution, which is to, in layout.tsx, just add const tailwindThemeEnabler = "dark" , and it works. The name of the constant is irreleavnt and it won't be used for anything. What's important is that "datk" is the value, so to trick Tailwind into generating styles for class "dark". Now I don't have to sync with localStorage. Class "dark" in <html > persists on navigation.
Woo-oo!
Apparently, it works!
After watching this video: https://www.youtube.com/watch?v=guh9qzxkb1o
I decided to try a solution, which is to, in layout.tsx, just add const tailwindThemeEnabler = "dark" , and it works. The name of the constant is irreleavnt and it won't be used for anything. What's important is that "datk" is the value, so to trick Tailwind into generating styles for class "dark". Now I don't have to sync with localStorage. Class "dark" in <html > persists on navigation.
Woo-oo!
Apparently, it works!