theme in storage

next.js (pages dir) + tailwind I want my user to click button. button adds a class of dark to the html tag, and tailwind updates all utilities with their dark counterpart. the button also sets local storage with a theme of dark. I want my user to leave or refresh the page to see their dark mode. the problem: when a user refreshes the page, the page loads as light mode then dark. I try to access my local storage beforehand or render null with code similar to this SO answer: https://stackoverflow.com/a/74134368 but that doesnt work. so how can I access my user's stored theme before a page first paints?
Stack Overflow
How to fix dark mode background color flicker in NextJS?
So my issue is that Next.js does not have access to localStorage on the client side and thus will ship HTML that by default either does or does not have class="dark". This means that when...
28 Replies
Sybatron
Sybatron2y ago
Dark Mode - Tailwind CSS
Using Tailwind CSS to style your site in dark mode.
Silas | @silaspath
this is understood. the 'class' option is selected
del.hydraz
del.hydraz2y ago
The way I usually do it is have a boolean in local storage that tracks the theme and a function that runs whenever the state of that boolean changes, adding the "dark" class to the body tag when dark=true and removing it when dark=false. You could also use whatever state management you'd like, so long as you can select the body tag and add that utility class. Does that help?
Silas | @silaspath
sorta. the problem was that the light them would flash before the browser did find the local storage and set it to dark
jdsl
jdsl2y ago
The server-side rendering doesn't know that user's local storage and you get hit with the light mode first.
Neto
Neto2y ago
you can use css root variables
Silas | @silaspath
though you all have nudged me in the right direction and I think I found the best answer... turns out I should put this code in _app ...
if (typeof window !== "undefined") {
let storedTheme = localStorage?.getItem("theme");
if (!storedTheme) storedTheme = "light";
document.documentElement.classList.add(storedTheme);
}
if (typeof window !== "undefined") {
let storedTheme = localStorage?.getItem("theme");
if (!storedTheme) storedTheme = "light";
document.documentElement.classList.add(storedTheme);
}
like edit globals.css?
Neto
Neto2y ago
https://github.com/STNeto1/tw-react-theme
theme: {
extend: {
colors: {
primary: 'var(--color-primary)',
secondary: 'var(--color-secondary)'
}
}
},
theme: {
extend: {
colors: {
primary: 'var(--color-primary)',
secondary: 'var(--color-secondary)'
}
}
},
useEffect(() => {
document.documentElement.style.setProperty('--color-primary', theme.primary)
document.documentElement.style.setProperty(
'--color-secondary',
theme.secondary
)
}, [theme])
useEffect(() => {
document.documentElement.style.setProperty('--color-primary', theme.primary)
document.documentElement.style.setProperty(
'--color-secondary',
theme.secondary
)
}, [theme])
about the flash on screen you will have to eat the flash, there is no way of using it without local storage and time to sync
del.hydraz
del.hydraz2y ago
Yeah, the way to deal with the flicker is to run the code server-side, but idk how to get the data from local storage to the server to render it correctly If you know this code runs on the server, then you should be all good and the flicker should go away
jdsl
jdsl2y ago
For SSR, you can set the theme preference in the user profile (db) or cookie
del.hydraz
del.hydraz2y ago
Ooh, cookie is a good idea
Silas | @silaspath
// imports~

if (typeof window !== "undefined") {
let storedTheme = localStorage?.getItem("theme");
if (!storedTheme) storedTheme = "light";

document.documentElement.classList.add(storedTheme);
}

const MyApp: AppType<{ session: Session | null }> = ({
Component,
pageProps: { session, ...pageProps },
}) => {
return (
<SessionProvider session={session}>
<Layout>
<Component {...pageProps} />
</Layout>
</SessionProvider>
);
};

export default api.withTRPC(MyApp);
// imports~

if (typeof window !== "undefined") {
let storedTheme = localStorage?.getItem("theme");
if (!storedTheme) storedTheme = "light";

document.documentElement.classList.add(storedTheme);
}

const MyApp: AppType<{ session: Session | null }> = ({
Component,
pageProps: { session, ...pageProps },
}) => {
return (
<SessionProvider session={session}>
<Layout>
<Component {...pageProps} />
</Layout>
</SessionProvider>
);
};

export default api.withTRPC(MyApp);
del.hydraz
del.hydraz2y ago
This is how I do it, but I'm running this function in an Astro script tag:
export const getTheme = () => {
const theme = (() => {
const localTheme = localStorage.getItem("theme");
if (localTheme) return localTheme;

return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
})();

const classList = document.documentElement.classList;

if (theme === "light") classList.remove("dark");
else classList.add("dark");
};
export const getTheme = () => {
const theme = (() => {
const localTheme = localStorage.getItem("theme");
if (localTheme) return localTheme;

return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
})();

const classList = document.documentElement.classList;

if (theme === "light") classList.remove("dark");
else classList.add("dark");
};
Silas | @silaspath
I'm more of a fan for this being sitewide
del.hydraz
del.hydraz2y ago
Correct me if I'm wrong, but I think storing it in a cookie will allow it to work sitewide because on each page request you can send the cookie along with the rest of the request. Next can then take that cookie and you can execute whatever utility class adding logic you'd like on server side
Silas | @silaspath
if I were to implement that, each page must then use getStaticProps? or would middleware be involved?
del.hydraz
del.hydraz2y ago
I think you'd either need getStaticProps for each page, or a way to inject it into the entire app each time, like using a layout where you have access to the body tag and can slap utility classes on it at will Sorry I'm still learning how Next handles its pages and layouts and such, so take what I say there with a grain of salt
jdsl
jdsl2y ago
Let me check an older project. I was doing it in the _app.tsx I think for the whole project You can also use middleware
Silas | @silaspath
thanks, so to make sure, it worked out for you?
jdsl
jdsl2y ago
CompanyApp.getInitialProps = async (context: AppContext) => {
const appProps = await App.getInitialProps(context);

// set existing cookies if found
const cookiesMap = parse(
context.ctx.req ? context.ctx.req.headers.cookie || "" : document.cookie
);
const cookies = resolveCookies(cookiesMap);

// consolidate for session state
const session: AppSession = {
theme: { mode: cookies.theme.mode },
layout: {
sidebarCollapsed: cookies.layout.sidebarCollapsed,
headerCollapsed: cookies.layout.headerCollapsed,
},
};

return {
...appProps,
session,
};
};
CompanyApp.getInitialProps = async (context: AppContext) => {
const appProps = await App.getInitialProps(context);

// set existing cookies if found
const cookiesMap = parse(
context.ctx.req ? context.ctx.req.headers.cookie || "" : document.cookie
);
const cookies = resolveCookies(cookiesMap);

// consolidate for session state
const session: AppSession = {
theme: { mode: cookies.theme.mode },
layout: {
sidebarCollapsed: cookies.layout.sidebarCollapsed,
headerCollapsed: cookies.layout.headerCollapsed,
},
};

return {
...appProps,
session,
};
};
I used this in a previous Next12 project (before T3) and it would keep the layouts without any flashing. Might not be the most elegant solution, but it's something to build from.
del.hydraz
del.hydraz2y ago
How did you handle it calling itself recursively? Sorry if a bit off-topic
jdsl
jdsl2y ago
Good catch, I renamed that before I posted. It was CompanyApp.getinitialProps =... But I removed company name before posting it here
del.hydraz
del.hydraz2y ago
Ah ok, yeah happy to
jdsl
jdsl2y ago
Fixed it These days I'd probably go with middleware if I already had middleware firing for each page
Silas | @silaspath
@joerambo thanks for the tips. I haven't implemented the middleware option yet but I commented in the code that I should much, much later an amazing amount of effort was put on this. more of a react problem than anything else haha.
jdsl
jdsl2y ago
Yeah I remember struggling with this concept earlier and trying out different ideas. Ran into things that worked, but would cause other problems down the line. I'm sure with RSC this will all change again shortly. (for the better)
del.hydraz
del.hydraz2y ago
All good if this is marked as the answer for indexing?
Want results from more Discord servers?
Add your server