Perfect theming for any React framework

SSR-friendly theming for React using cookies - with system preference, cross-tab sync, no flash, and a strongly typed useTheme hook.

bun add ssr-themes
Theme picker
Pick a palette and the UI updates instantly.
Hydration-safe changes that sync across tabs.
Usage example
Drop in themeScript() + ThemeProvider to get no-flash theming.
// src/routes/__root.tsx
import {createServerFn} from '@tanstack/react-start';
import {getRequestHeader} from '@tanstack/react-start/server';
import {
  HeadContent,
  Outlet,
  ScriptOnce,
  Scripts,
  createRootRoute,
} from '@tanstack/react-router';
import {ThemeProvider} from 'ssr-themes';
import {
  registerTheme,
  themeFromCookieHeader,
  themeScript,
} from 'ssr-themes/server';

const getInitialTheme = createServerFn({method: 'GET'}).handler(
  () => themeFromCookieHeader(getRequestHeader('cookie')),
);

function RootComponent() {
  const {initialTheme} = Route.useLoaderData();
  const theme =
    initialTheme && initialTheme !== 'system'
      ? initialTheme
      : undefined;

  return (
    <html suppressHydrationWarning {...registerTheme({theme})}>
      <head>
        <HeadContent />
      </head>
      <body>
        <ThemeProvider initialTheme={initialTheme}>
          <ScriptOnce children={themeScript()} />
          <Outlet />
        </ThemeProvider>
        <Scripts />
      </body>
    </html>
  );
}

export const Route = createRootRoute({
  loader: async () => ({
    initialTheme: await getInitialTheme(),
  }),
  component: RootComponent,
});
Forced theme routes
Use route staticData to force a theme per route.
// src/routes/dark.tsx
import {createFileRoute} from '@tanstack/react-router';

export const Route = createFileRoute('/dark')({
  staticData: {theme: 'dark'},
  component: () => <div>Always dark</div>,
});

// src/routes/__root.tsx
import {useMatches} from '@tanstack/react-router';
import {ThemeProvider, type SystemTheme} from 'ssr-themes';

const matches = useMatches();
const forcedTheme = matches.reduce<SystemTheme | undefined>(
  (theme, match) => {
    const staticData = match.staticData as
      | {theme?: SystemTheme}
      | undefined;
    return staticData?.theme ?? theme;
  },
  undefined,
);

<ThemeProvider forcedTheme={forcedTheme}>{children}</ThemeProvider>;