Skip to main content

Overview

Kuzenbo’s theme runtime provides a complete solution for managing light/dark mode, semantic design tokens, and flicker-free theme initialization. Built on @kuzenbo/theme, it handles theme persistence, server-side rendering, and seamless client-side hydration.

Installation

npm install @kuzenbo/theme
The theme package requires React 19+ and next-themes as peer dependencies.

Quick start

Set up the theme runtime in your root layout:
app/layout.tsx
import type { ReactNode } from "react";

import "@kuzenbo/theme/prebuilt/kuzenbo.css";
import { ThemeBootstrapScript } from "@kuzenbo/theme";
import { ThemeProvider } from "@kuzenbo/theme";

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body>
        <ThemeBootstrapScript />
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  );
}
Always include suppressHydrationWarning on the html element to prevent React hydration warnings caused by the theme bootstrap script.

Core components

ThemeBootstrapScript

The ThemeBootstrapScript component injects an inline script that runs before React hydrates, preventing theme flicker. It reads the theme preference from cookies and localStorage, then applies it to the document root.
import { ThemeBootstrapScript } from "@kuzenbo/theme";

export default function RootLayout({ children }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <head>
        <ThemeBootstrapScript />
      </head>
      <body>{children}</body>
    </html>
  );
}

Props

defaultThemeSetting
ThemeSetting
default:"system"
The default theme setting when no preference is stored. Accepts "light", "dark", or "system".
id
string
default:"kuzenbo-theme-bootstrap"
The HTML id attribute for the script element.
nonce
string
CSP nonce for the inline script. Reads from process.env.THEME_BOOTSTRAP_NONCE by default.

ThemeProvider

The ThemeProvider wraps your application and manages theme state using next-themes. It syncs with localStorage and provides theme toggling functionality.
import { ThemeProvider } from "@kuzenbo/theme";

export default function RootLayout({ children }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  );
}

Props

ThemeProvider accepts all next-themes props. Key defaults:
attribute
string
default:"class"
HTML attribute to apply theme. Defaults to class (adds .dark class).
defaultTheme
string
default:"system"
Default theme when no preference is stored.
storageKey
string
default:"kuzenbo-theme"
localStorage key for persisting theme preference.
enableSystem
boolean
default:"true"
Enable system theme detection.
disableTransitionOnChange
boolean
default:"true"
Disable CSS transitions when theme changes to prevent visual artifacts.

Theme resolution flow

Kuzenbo resolves theme preferences in this priority order:
1

Cookie preference

First checks the kuzenbo-theme cookie for a stored preference (dark or light).
2

localStorage preference

If no cookie exists, reads from localStorage using the kuzenbo-theme key.
3

System preference

Falls back to the system color scheme using (prefers-color-scheme: dark) media query.
4

Default setting

Uses the configured defaultThemeSetting (defaults to "system").

Bootstrap synchronization

The bootstrap script ensures consistency between cookie and localStorage:
// If cookie exists but localStorage differs, sync to cookie value
if (cookieTheme && storageTheme !== cookieTheme) {
  localStorage.setItem(STORAGE_KEY, cookieTheme);
}

// If localStorage exists but no cookie, create cookie
if (storageTheme && !cookieTheme) {
  document.cookie = `kuzenbo-theme=${storageTheme}; Path=/; Max-Age=31536000; SameSite=Lax`;
}

Programmatic utilities

applyThemeToRootElement

Manually apply a theme to the document root:
import { applyThemeToRootElement } from "@kuzenbo/theme";

applyThemeToRootElement("dark");
// Adds .dark class and sets color-scheme: dark

resolveThemeBootstrapPlan

Resolve the theme bootstrap plan for server-side rendering:
import { resolveThemeBootstrapPlan } from "@kuzenbo/theme";

const plan = resolveThemeBootstrapPlan({
  cookieTheme: "dark",
  storageTheme: null,
  systemTheme: "light",
  defaultThemeSetting: "system",
});

console.log(plan);
// {
//   resolvedTheme: "dark",
//   shouldWriteCookie: false,
//   shouldWriteStorage: true
// }

readThemeFromCookieString

Extract theme preference from a cookie string:
import { readThemeFromCookieString } from "@kuzenbo/theme";

const theme = readThemeFromCookieString("kuzenbo-theme=dark; Path=/");
console.log(theme); // "dark"

serializeThemeCookie

Create a theme cookie string:
import { serializeThemeCookie } from "@kuzenbo/theme";

const cookieString = serializeThemeCookie("dark");
console.log(cookieString);
// "kuzenbo-theme=dark; Path=/; Max-Age=31536000; SameSite=Lax"

Server-side integration

Next.js App Router

Read theme from cookies in Server Components:
app/layout.tsx
import { cookies } from "next/headers";
import { readThemeFromCookieString } from "@kuzenbo/theme";

export default async function RootLayout({ children }) {
  const cookieStore = await cookies();
  const theme = readThemeFromCookieString(cookieStore.toString());
  
  return (
    <html lang="en" suppressHydrationWarning data-theme={theme}>
      <body>
        <ThemeBootstrapScript defaultThemeSetting="dark" />
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  );
}

Next.js middleware

Persist theme changes via middleware:
middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { readThemeFromCookieString, serializeThemeCookie } from "@kuzenbo/theme";

export function middleware(request: NextRequest) {
  const response = NextResponse.next();
  const theme = readThemeFromCookieString(request.headers.get("cookie") || "");
  
  if (!theme) {
    response.headers.set("Set-Cookie", serializeThemeCookie("system"));
  }
  
  return response;
}

Constants and configuration

Storage keys

import {
  THEME_COOKIE_KEY,
  THEME_STORAGE_KEY,
  THEME_COOKIE_MAX_AGE_SECONDS,
} from "@kuzenbo/theme";

console.log(THEME_COOKIE_KEY); // "kuzenbo-theme"
console.log(THEME_STORAGE_KEY); // "kuzenbo-theme"
console.log(THEME_COOKIE_MAX_AGE_SECONDS); // 31536000 (1 year)

System theme detection

import { SYSTEM_DARK_MEDIA_QUERY } from "@kuzenbo/theme";

console.log(SYSTEM_DARK_MEDIA_QUERY); // "(prefers-color-scheme: dark)"

Default values

import {
  DEFAULT_THEME_SETTING,
  THEME_BOOTSTRAP_SCRIPT_ID,
} from "@kuzenbo/theme";

console.log(DEFAULT_THEME_SETTING); // "system"
console.log(THEME_BOOTSTRAP_SCRIPT_ID); // "kuzenbo-theme-bootstrap"

CSP configuration

For strict Content Security Policy, provide a nonce:
// Set via environment variable
process.env.THEME_BOOTSTRAP_NONCE = "abc123";
import { ThemeBootstrapScript } from "@kuzenbo/theme";

<ThemeBootstrapScript nonce="abc123" />
The bootstrap script will include the nonce attribute:
<script id="kuzenbo-theme-bootstrap" nonce="abc123">
  (function () { /* theme bootstrap code */ })();
</script>

Type reference

ThemePreference

type ThemePreference = "dark" | "light";

ThemeSetting

type ThemeSetting = ThemePreference | "system";

ThemeBootstrapPlan

interface ThemeBootstrapPlan {
  resolvedTheme: ThemePreference;
  shouldWriteCookie: boolean;
  shouldWriteStorage: boolean;
}

ThemeBootstrapPlanInput

interface ThemeBootstrapPlanInput {
  cookieTheme: ThemePreference | null;
  defaultThemeSetting?: ThemeSetting;
  storageTheme: ThemePreference | null;
  systemTheme: ThemePreference;
}

Best practices

The theme bootstrap script modifies the HTML element before React hydrates. Without suppressHydrationWarning, React will log warnings about mismatched content.
<html lang="en" suppressHydrationWarning>
The bootstrap script should run as early as possible to prevent theme flicker. Place it in the <head> or at the start of <body>.
<html lang="en" suppressHydrationWarning>
  <head>
    <ThemeBootstrapScript />
  </head>
  <body>...</body>
</html>
Always use semantic tokens like --kb-background and --kb-foreground instead of hardcoding theme colors. This ensures your UI adapts correctly to theme changes.
.my-component {
  background-color: var(--kb-background);
  color: var(--kb-foreground);
}
The default disableTransitionOnChange prevents visual artifacts when switching themes. Keep this enabled unless you have specific transition requirements.

Styles baseline

Learn about global baseline styles and CSS foundations.

Architecture

Dive into Kuzenbo’s monorepo structure and package boundaries.

Build docs developers (and LLMs) love