diff --git a/apps/v4/content/docs/dark-mode/index.mdx b/apps/v4/content/docs/dark-mode/index.mdx index de7b6957f6b..72d54caf4eb 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 86a47e4c7f3..950e75665e5 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 00000000000..36198b41ec2 --- /dev/null +++ b/apps/v4/content/docs/dark-mode/tanstack-start.mdx @@ -0,0 +1,191 @@ +--- +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 +} + +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) + setThemeState( + stored === "light" || stored === "dark" || stored === "system" + ? stored + : defaultTheme + ) + setMounted(true) + }, [defaultTheme, storageKey]) + + useEffect(() => { + if (!mounted) return + applyTheme(theme) + }, [theme, mounted]) + + useEffect(() => { + if (!mounted || theme !== "system") return + + const media = window.matchMedia("(prefers-color-scheme: dark)") + const onChange = () => applyTheme("system") + media.addEventListener("change", onChange) + return () => media.removeEventListener("change", onChange) + }, [theme, mounted]) + + const setTheme = (next: Theme) => { + localStorage.setItem(storageKey, next) + setThemeState(next) + } + + return ( + + {getThemeScript(storageKey, defaultTheme)} + {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 + + + + ) +} +``` + +