uni-themes

React Router

Framework mode (the Remix successor) with cookie storage and loader reading

Use cookie storage so the root loader can read the theme on the server:

app/root.tsx
import { ThemeProvider } from 'uni-themes'
import { ThemeScript } from 'uni-themes/script'
import { getThemeFromRequest } from 'uni-themes/server'
import { Links, Meta, Outlet, Scripts, ScrollRestoration } from 'react-router'
import type { LoaderFunctionArgs } from 'react-router'

export function loader({ request }: LoaderFunctionArgs) {
  return { theme: getThemeFromRequest(request) } // null until the user picks
}

export default function App() {
  return (
    <html lang="en" suppressHydrationWarning>
      <head>
        <meta charSet="utf-8" />
        <ThemeScript storage="cookie" />
        <Meta />
        <Links />
      </head>
      <body>
        <ThemeProvider storage="cookie">
          <Outlet />
        </ThemeProvider>
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  )
}

Components use useTheme() from uni-themes as usual — no 'use client' needed outside RSC frameworks.

The playground runs this end to end.