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-themes
Theme 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,
});