uni-themes

Next.js

App Router setup with a server-rendered ThemeScript

app/layout.tsx
// stays a server component
import { ThemeProvider } from 'uni-themes'
import { ThemeScript } from 'uni-themes/script'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en" suppressHydrationWarning>
      <head>
        <ThemeScript />
      </head>
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  )
}
components/theme-switcher.tsx
'use client'
import { useTheme } from 'uni-themes'

export function ThemeSwitcher() {
  const { resolvedTheme, setTheme } = useTheme()
  return (
    <button
      onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}
    >
      Toggle
    </button>
  )
}

Reading the theme on the server

Switch on cookie storage and use uni-themes/next:

app/layout.tsx
<ThemeScript storage="cookie" />
// ...
<ThemeProvider storage="cookie">{children}</ThemeProvider>
import { getTheme } from 'uni-themes/next'

// in a server component, route handler or server action
const theme = await getTheme() // 'dark' | 'light' | 'system' | null

getTheme() returns null for first-time visitors — the inline script falls back to the OS preference on the client.

Using with fumadocs

fumadocs' RootProvider bundles its own next-themes integration; disable it and let uni-themes own the <html> class (this site does exactly that):

<RootProvider theme={{ enabled: false }}>{children}</RootProvider>

On this page