From a757e80242afd0fd694ce1579d79b93b2e99cc69 Mon Sep 17 00:00:00 2001 From: Ray Date: Tue, 14 Apr 2026 15:29:26 -0400 Subject: [PATCH 1/2] docs(dark-mode): add tanstack start guide --- apps/v4/content/docs/dark-mode/index.mdx | 16 ++ apps/v4/content/docs/dark-mode/meta.json | 2 +- .../content/docs/dark-mode/tanstack-start.mdx | 183 ++++++++++++++++++ 3 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 apps/v4/content/docs/dark-mode/tanstack-start.mdx diff --git a/apps/v4/content/docs/dark-mode/index.mdx b/apps/v4/content/docs/dark-mode/index.mdx index de7b6957f..72d54caf4 100644 --- a/apps/v4/content/docs/dark-mode/index.mdx +++ b/apps/v4/content/docs/dark-mode/index.mdx @@ -56,4 +56,20 @@ description: Adding dark mode to your site.

Remix

+ + + TanStack + + +

TanStack Start

+
diff --git a/apps/v4/content/docs/dark-mode/meta.json b/apps/v4/content/docs/dark-mode/meta.json index 86a47e4c7..950e75665 100644 --- a/apps/v4/content/docs/dark-mode/meta.json +++ b/apps/v4/content/docs/dark-mode/meta.json @@ -1,4 +1,4 @@ { "title": "Dark mode", - "pages": ["index", "next", "vite", "astro", "remix"] + "pages": ["index", "next", "vite", "astro", "remix", "tanstack-start"] } diff --git a/apps/v4/content/docs/dark-mode/tanstack-start.mdx b/apps/v4/content/docs/dark-mode/tanstack-start.mdx new file mode 100644 index 000000000..58d504338 --- /dev/null +++ b/apps/v4/content/docs/dark-mode/tanstack-start.mdx @@ -0,0 +1,183 @@ +--- +title: TanStack Start +description: Adding dark mode to your TanStack Start app. +--- + + + +## Create a theme provider + +TanStack Start uses `ScriptOnce` from `@tanstack/react-router` to inject a script that runs before React hydrates, preventing flash of unstyled content (FOUC). + +```tsx title="components/theme-provider.tsx" showLineNumbers +import { createContext, useContext, useEffect, useState } from "react" +import { ScriptOnce } from "@tanstack/react-router" + +type Theme = "dark" | "light" | "system" + +type ThemeProviderProps = { + children: React.ReactNode + defaultTheme?: Theme + storageKey?: string +} + +type ThemeProviderState = { + theme: Theme + setTheme: (theme: Theme) => void +} + +const themeScript = `(function(){try{var t=localStorage.getItem('theme');if(t!=='light'&&t!=='dark'&&t!=='system'){t='system'}var d=matchMedia('(prefers-color-scheme: dark)').matches;var r=t==='system'?(d?'dark':'light'):t;var e=document.documentElement;e.classList.add(r);e.style.colorScheme=r}catch(e){}})();` + +const ThemeProviderContext = createContext({ + theme: "system", + setTheme: () => {}, +}) + +export function ThemeProvider({ + children, + defaultTheme = "system", + storageKey = "theme", +}: ThemeProviderProps) { + const [theme, setThemeState] = useState(defaultTheme) + + useEffect(() => { + const stored = localStorage.getItem(storageKey) + if (stored === "light" || stored === "dark" || stored === "system") { + setThemeState(stored) + } + }, [storageKey]) + + useEffect(() => { + const root = document.documentElement + root.classList.remove("light", "dark") + + const resolved = + theme === "system" + ? window.matchMedia("(prefers-color-scheme: dark)").matches + ? "dark" + : "light" + : theme + + root.classList.add(resolved) + root.style.colorScheme = resolved + }, [theme]) + + useEffect(() => { + if (theme !== "system") return undefined + + const media = window.matchMedia("(prefers-color-scheme: dark)") + const onChange = () => { + const root = document.documentElement + root.classList.remove("light", "dark") + const resolved = media.matches ? "dark" : "light" + root.classList.add(resolved) + root.style.colorScheme = resolved + } + media.addEventListener("change", onChange) + return () => media.removeEventListener("change", onChange) + }, [theme]) + + const setTheme = (next: Theme) => { + localStorage.setItem(storageKey, next) + setThemeState(next) + } + + return ( + + {themeScript} + {children} + + ) +} + +export function useTheme() { + const context = useContext(ThemeProviderContext) + if (context === undefined) + throw new Error("useTheme must be used within a ThemeProvider") + return context +} +``` + +## Wrap your root layout + +Add the `ThemeProvider` to your root layout and add the `suppressHydrationWarning` prop to the `html` tag. + +```tsx {8,19,24-26} title="src/routes/__root.tsx" showLineNumbers +import { + createRootRoute, + HeadContent, + Outlet, + Scripts, +} from "@tanstack/react-router" + +import { ThemeProvider } from "@/components/theme-provider" + +export const Route = createRootRoute({ + head: () => ({ + // ... + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + + + ) +} +``` + +## Add a mode toggle + +Place a mode toggle on your site to toggle between light and dark mode. + +```tsx title="components/mode-toggle.tsx" showLineNumbers +import { Moon, Sun } from "lucide-react" + +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { useTheme } from "@/components/theme-provider" + +export function ModeToggle() { + const { setTheme } = useTheme() + + return ( + + + + + + setTheme("light")}> + Light + + setTheme("dark")}> + Dark + + setTheme("system")}> + System + + + + ) +} +``` + + From 01539fb4d735788d305f895899548ee77dfad60f Mon Sep 17 00:00:00 2001 From: shadcn Date: Tue, 21 Apr 2026 10:35:34 +0400 Subject: [PATCH 2/2] refactor: add getThemeScript --- .../content/docs/dark-mode/tanstack-start.mdx | 70 +++++++++++-------- 1 file changed, 39 insertions(+), 31 deletions(-) diff --git a/apps/v4/content/docs/dark-mode/tanstack-start.mdx b/apps/v4/content/docs/dark-mode/tanstack-start.mdx index 58d504338..36198b41e 100644 --- a/apps/v4/content/docs/dark-mode/tanstack-start.mdx +++ b/apps/v4/content/docs/dark-mode/tanstack-start.mdx @@ -5,7 +5,7 @@ description: Adding dark mode to your TanStack Start app. -## Create a theme provider +### Create a theme provider TanStack Start uses `ScriptOnce` from `@tanstack/react-router` to inject a script that runs before React hydrates, preventing flash of unstyled content (FOUC). @@ -26,56 +26,64 @@ type ThemeProviderState = { setTheme: (theme: Theme) => void } -const themeScript = `(function(){try{var t=localStorage.getItem('theme');if(t!=='light'&&t!=='dark'&&t!=='system'){t='system'}var d=matchMedia('(prefers-color-scheme: dark)').matches;var r=t==='system'?(d?'dark':'light'):t;var e=document.documentElement;e.classList.add(r);e.style.colorScheme=r}catch(e){}})();` +function getThemeScript(storageKey: string, defaultTheme: Theme) { + const key = JSON.stringify(storageKey) + const fallback = JSON.stringify(defaultTheme) + + return `(function(){try{var t=localStorage.getItem(${key});if(t!=='light'&&t!=='dark'&&t!=='system'){t=${fallback}}var d=matchMedia('(prefers-color-scheme: dark)').matches;var r=t==='system'?(d?'dark':'light'):t;var e=document.documentElement;e.classList.add(r);e.style.colorScheme=r}catch(e){}})();` +} const ThemeProviderContext = createContext({ theme: "system", setTheme: () => {}, }) +function applyTheme(theme: Theme) { + const root = document.documentElement + root.classList.remove("light", "dark") + + const resolved = + theme === "system" + ? window.matchMedia("(prefers-color-scheme: dark)").matches + ? "dark" + : "light" + : theme + + root.classList.add(resolved) + root.style.colorScheme = resolved +} + export function ThemeProvider({ children, defaultTheme = "system", storageKey = "theme", }: ThemeProviderProps) { const [theme, setThemeState] = useState(defaultTheme) + const [mounted, setMounted] = useState(false) useEffect(() => { const stored = localStorage.getItem(storageKey) - if (stored === "light" || stored === "dark" || stored === "system") { - setThemeState(stored) - } - }, [storageKey]) + setThemeState( + stored === "light" || stored === "dark" || stored === "system" + ? stored + : defaultTheme + ) + setMounted(true) + }, [defaultTheme, storageKey]) useEffect(() => { - const root = document.documentElement - root.classList.remove("light", "dark") - - const resolved = - theme === "system" - ? window.matchMedia("(prefers-color-scheme: dark)").matches - ? "dark" - : "light" - : theme - - root.classList.add(resolved) - root.style.colorScheme = resolved - }, [theme]) + if (!mounted) return + applyTheme(theme) + }, [theme, mounted]) useEffect(() => { - if (theme !== "system") return undefined + if (!mounted || theme !== "system") return const media = window.matchMedia("(prefers-color-scheme: dark)") - const onChange = () => { - const root = document.documentElement - root.classList.remove("light", "dark") - const resolved = media.matches ? "dark" : "light" - root.classList.add(resolved) - root.style.colorScheme = resolved - } + const onChange = () => applyTheme("system") media.addEventListener("change", onChange) return () => media.removeEventListener("change", onChange) - }, [theme]) + }, [theme, mounted]) const setTheme = (next: Theme) => { localStorage.setItem(storageKey, next) @@ -84,7 +92,7 @@ export function ThemeProvider({ return ( - {themeScript} + {getThemeScript(storageKey, defaultTheme)} {children} ) @@ -98,7 +106,7 @@ export function useTheme() { } ``` -## Wrap your root layout +### Wrap your root layout Add the `ThemeProvider` to your root layout and add the `suppressHydrationWarning` prop to the `html` tag. @@ -136,7 +144,7 @@ function RootComponent() { } ``` -## Add a mode toggle +### Add a mode toggle Place a mode toggle on your site to toggle between light and dark mode.