T
TanStack•7mo ago
vicious-gold

Dark Mode

Has anyone created the Theme Provider ShadCN suggests for the "Vite" solution? Where did you wrap? I tried wrapping..
function RootComponent() {
return (
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
<RootDocument>
<Outlet />
</RootDocument>
</ThemeProvider>
);
}
function RootComponent() {
return (
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
<RootDocument>
<Outlet />
</RootDocument>
</ThemeProvider>
);
}
and it threw an error that localStorage is not defined. I believe this is because it was running on the server?
27 Replies
fair-rose
fair-rose•7mo ago
yes this runs on the server during SSR
fair-rose
fair-rose•7mo ago
checkout https://github.com/ally-ahmed/tss-blog-starter on how to do this in start
GitHub
GitHub - ally-ahmed/tss-blog-starter: A blog/portfolio starter proj...
A blog/portfolio starter project built with TanStack Start. - ally-ahmed/tss-blog-starter
vicious-gold
vicious-goldOP•7mo ago
Sheeeeeesh. Bro, you're on the $$! Fast AF 🙂
vicious-gold
vicious-goldOP•7mo ago
Hmm, seems this impl causes a hydration
hook.js:608 A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used: - A server/client branch if (typeof window !== 'undefined'). - Variable input such as Date.now() or Math.random() which changes each time it's called. - Date formatting in a user's locale which doesn't match the server. - External changing data without sending a snapshot of it along with the HTML. - Invalid HTML tag nesting. It can also happen if the client has a browser extension installed which messes with the HTML before React loaded. https://react.dev/link/hydration-mismatch ... <MatchesInner> <CatchBoundary getResetKey={function getResetKey} errorComponent={function ErrorComponent} ...> <CatchBoundaryImpl getResetKey={function getResetKey} onCatch={function onCatch}> <MatchImpl matchId="root"> <SafeFragment fallback={null}> <CatchBoundary getResetKey={function getResetKey} errorComponent={function errorComponent} ...> <CatchBoundaryImpl getResetKey={function getResetKey} onCatch={function onCatch}> <CatchNotFound fallback={function fallback}> <CatchBoundary getResetKey={function getResetKey} onCatch={function onCatch} ...> <CatchBoundaryImpl getResetKey={function getResetKey} onCatch={function onCatch}> <MatchInnerImpl matchId="root"> <RootComponent> <RootDocument> <html - data-theme="dark" - style={{color-scheme:"dark"}} >
hydrateRoot – React
The library for web and native user interfaces
vicious-gold
vicious-goldOP•7mo ago
any thoughts before I dive in?
fair-rose
fair-rose•7mo ago
thats unavoidable the server cannot possibly know your system theme so you need to accept that and ignore that hydration error / suppress
vicious-gold
vicious-goldOP•7mo ago
okay, so safe to ignore this one here?
fair-rose
fair-rose•7mo ago
yes
vicious-gold
vicious-goldOP•7mo ago
Perfect, I think it just threw a flag - my guess is nextjs or something there squashes this type of error?
fair-rose
fair-rose•7mo ago
in which way?
vicious-gold
vicious-goldOP•7mo ago
I just had never experienced this hydration error in nextjs with theming
fair-rose
fair-rose•7mo ago
GitHub
GitHub - pacocoursey/next-themes: Perfect Next.js dark mode in 2 li...
Perfect Next.js dark mode in 2 lines of code. Support System preference and any other theme with no flashing - pacocoursey/next-themes
fair-rose
fair-rose•7mo ago
No description
vicious-gold
vicious-goldOP•7mo ago
AH, there it is. That's what I was poking at great shout
plain-purple
plain-purple•7mo ago
Just as a note we allow our users to change the theme manually too. Once they do that we save a cookie which allows accessing it on the server so the initial theme used for SSR is already the right one 🙂 For system ofc this does not work 😄
fair-rose
fair-rose•7mo ago
but you dont need the cookie just store in localStorage this just prevents the hydration error that is suppressed anyhow for the initial load where no theme is known
plain-purple
plain-purple•7mo ago
but if i use localStorage I can't know what the right theme is on the server right? So i get hydration error and possibly also a flash of the wrong UI while it changes the theme UI on the client?
fair-rose
fair-rose•7mo ago
you can never know the theme on the server for the initial load
plain-purple
plain-purple•7mo ago
i run this in my beforeLoad:
export const getThemeCookie = (): ThemeModeOptions => {
return createIsomorphicFn()
.server(() => {
const cookie = getCookie("theme-mode");
return (cookie as ThemeModeOptions) || "system";
})
.client(() => {
const cookie = document.cookie.split("; ").find((row) => row.startsWith("theme-mode="));
return (cookie?.split("=")[1] || "system") as ThemeModeOptions;
})();
};
export const getThemeCookie = (): ThemeModeOptions => {
return createIsomorphicFn()
.server(() => {
const cookie = getCookie("theme-mode");
return (cookie as ThemeModeOptions) || "system";
})
.client(() => {
const cookie = document.cookie.split("; ").find((row) => row.startsWith("theme-mode="));
return (cookie?.split("=")[1] || "system") as ThemeModeOptions;
})();
};
And then use that value and pass it into my MUI ThemeProvider. So if the user has already choosen a theme in the past (e.g. manually switching to high-contrast or dark) I can know the theme on the server at the initial load? Or am i missing something. I know that this will never work on the very very first load when the user visits my app the very first time.
fair-rose
fair-rose•7mo ago
you just need to ensure that you run the script that reads from local storage executes early enough then you wont get a flash
plain-purple
plain-purple•7mo ago
I might try that again, but this now works pretty good so I am inclined to just leave it for now as long as you don't tell me that this is garbage 😄 already spent too much time dealing with hydration errors.
fair-rose
fair-rose•7mo ago
it's not garbage. we also use a cookie on tanstack.com for theming just wanted to point out you can do it purely client side see this example above
plain-purple
plain-purple•7mo ago
ok nice! 🙂
wise-white
wise-white•7mo ago
i have
vicious-gold
vicious-goldOP•7mo ago
Gonna try working through this tonight now that ShadCN supports tailwind v4 Has anyone does this with Tailwind V4 yet? Okay, I have this working now with V4. Here's a different approach than the ThemeProvider...
function RootDocument({ children }: Readonly<{ children: ReactNode }>) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<HeadContent />
</head>
<body>
<ScriptOnce>
{`document.documentElement.classList.toggle(
'dark',
localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)
)`}
</ScriptOnce>
{children}

<Scripts />
</body>
</html>
);
}
function RootDocument({ children }: Readonly<{ children: ReactNode }>) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<HeadContent />
</head>
<body>
<ScriptOnce>
{`document.documentElement.classList.toggle(
'dark',
localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)
)`}
</ScriptOnce>
{children}

<Scripts />
</body>
</html>
);
}
vicious-gold
vicious-goldOP•7mo ago
@Manuel Schiller Here's a NICE starter^ was really helpful getting tailwind v4 up and running too: https://github.com/dotnize/tanstarter/tree/main
GitHub
GitHub - dotnize/tanstarter: minimal TanStack Start template with B...
minimal TanStack Start template with Better Auth, Drizzle ORM, shadcn/ui - dotnize/tanstarter
wise-white
wise-white•7mo ago
I also had to add this in to my Head to stop the flash as well as put ThemeProvider around the Body tag. As of now i have no problems anymore
script:[ {children:`(function() {
const storedTheme = localStorage.getItem('vite-ui-theme');
if (storedTheme === 'dark' || (!storedTheme && 'dark' === 'dark')) { //handle first time load, if there is nothing in localstorage.
document.documentElement.classList.add('dark');
} else if (storedTheme === 'light'){
document.documentElement.classList.add('light');
} else if (storedTheme === 'system'){
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
document.documentElement.classList.add(systemTheme)
}
})()`}]
script:[ {children:`(function() {
const storedTheme = localStorage.getItem('vite-ui-theme');
if (storedTheme === 'dark' || (!storedTheme && 'dark' === 'dark')) { //handle first time load, if there is nothing in localstorage.
document.documentElement.classList.add('dark');
} else if (storedTheme === 'light'){
document.documentElement.classList.add('light');
} else if (storedTheme === 'system'){
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
document.documentElement.classList.add(systemTheme)
}
})()`}]

Did you find this page helpful?