SSR-safe dark mode and theming
Theming for apps where the theme needs to affect server-rendered HTML, not just client state.
bun add ssr-themesTheme picker
Pick a palette and the UI updates instantly.
Hydration-safe changes that sync across tabs.
SSR setup
Read the cookie on the server, pre-render
registerTheme() + themeScript(), then hydrate ThemeProvider.// src/routes/__root.tsx
import {
HeadContent,
Outlet,
ScriptOnce,
Scripts,
createRootRoute,
} from '@tanstack/react-router';
import {createServerFn} from '@tanstack/react-start';
import {getRequestHeader} from '@tanstack/react-start/server';
import {
registerTheme,
ThemeProvider,
parseThemeCookie,
themeScript,
} from '../lib/theme';
const getThemeState = createServerFn({
method: 'GET',
}).handler(() =>
parseThemeCookie(getRequestHeader('cookie')),
);
function RootComponent() {
const {themeState} = Route.useLoaderData();
return (
<html {...registerTheme(themeState)}>
<head>
<HeadContent />
</head>
<body>
<ThemeProvider {...(themeState ?? {})}>
<ScriptOnce children={themeScript()} />
<Outlet />
</ThemeProvider>
<Scripts />
</body>
</html>
);
}
export const Route = createRootRoute({
loader: async () => ({
themeState: await getThemeState(),
}),
component: RootComponent,
});
Theme switcher
Bind the React helper once, then call useTheme() in a route component.
// src/lib/theme.ts
import {createTheme} from 'ssr-themes';
import {bindTheme} from 'ssr-themes/react';
export const {
options,
registerTheme,
parseThemeCookie,
themeScript,
} = createTheme();
export const {ThemeProvider, useTheme} =
bindTheme(options);
// src/routes/index.tsx
import {createFileRoute} from '@tanstack/react-router';
import {useTheme} from '../lib/theme';
type ThemeName =
| 'system'
| 'dark'
| 'light';
function Home() {
const {selected, setSelected} = useTheme();
return (
<select
value={selected ?? 'system'}
onChange={event =>
setSelected(event.target.value as ThemeName)
}
>
<option value="system">System</option>
<option value="dark">Dark</option>
<option value="light">Light</option>
</select>
);
}
export const Route = createFileRoute('/')({
component: Home,
});