Documentation index for AI agents (llms.txt). Markdown versions of every page are available by appending .md to the page URL. The full corpus is at /llms-full.txt.

Dark Mode

Cherry provides built-in support for dark mode. Pass the themeDark prop to the CherryThemeProvider component:

import {
  CherryThemeProvider,
  theme,
  themeDark,
} from "cherry-styled-components";

export default function App({ children }) {
  return (
    <CherryThemeProvider theme={theme} themeDark={themeDark}>
      {children}
    </CherryThemeProvider>
  );
}

Automatic Detection

Cherry will automatically check whether your system has dark mode enabled. If detected, the default theme will be set to dark. This only happens if you passed the themeDark prop to CherryThemeProvider.

Manual Theme Toggle

The easiest way to let users switch themes is the built-in ThemeToggle component. It calls toggleTheme() from ThemeContext and persists the choice automatically:

import { ThemeToggle } from "cherry-styled-components";

export default function Header() {
  return (
    <header>
      <ThemeToggle />
    </header>
  );
}

To add a keyboard shortcut, set the $shortcut prop. While the toggle is mounted, Command + Shift + L (Ctrl + Shift + L on Windows) flips the theme, using the same toggleTheme and persistence as a click:

<ThemeToggle $shortcut />

For a custom control, call toggleTheme from ThemeContext yourself. It persists the choice to localStorage and keeps the html dark class in sync. Under CherryThemeProvider, setTheme is the raw state setter and does neither, so the choice is lost on reload; under ClientThemeProvider (covered below), setTheme also persists:

import { ThemeContext } from "cherry-styled-components";
import { useContext } from "react";

export default function Header() {
  const { toggleTheme } = useContext(ThemeContext);
  return (
    <header>
      <button onClick={toggleTheme}>Switch Theme</button>
    </header>
  );
}

Flash-Free Dark Mode with SSR

CherryThemeProvider resolves the theme on the client after mount, so in a server-rendered app the first paint is always the light theme, which dark mode users see as a flash. For server-rendered apps (e.g. Next.js App Router), use ClientThemeProvider instead:

  1. The server reads the theme cookie and passes the verdict as $initial, so the first paint is already correct.
  2. themeInitScript, a blocking script in the document head, seeds the cookie from the OS preference on first visits from browsers without color-scheme client hints (Safari, Firefox), hides the body until the client has applied the right theme, and rewrites the theme-color meta tag so the browser chrome doesn't hold the light tint either.
  3. On mount, the provider reconciles the server's guess against the cookie and OS preference and swaps the theme in place if needed.
Request page with the theme cookie, if set HTML rendered in the right theme ($initial from the cookie) Mount Reconcile the cookie and OS preference Swap the theme in place only if the first paint was wrong First visit without a cookie? themeInitScript seeds it from the OS preference and hides the body until the theme is applied Browser Server ClientThemeProvider

Theme changes persist to the theme cookie and localStorage directly. No API route is needed. The layout below also wraps the app in StyledComponentsRegistry, the SSR style registry introduced in Installation:

app/layout.tsx
// Server component
import { cookies } from "next/headers";
import { StyledComponentsRegistry } from "cherry-styled-components/next";
import { ClientThemeProvider, themeInitScript } from "cherry-styled-components";
import { theme, themeDark } from "./theme";

export default async function RootLayout({ children }) {
  const cookieTheme = (await cookies()).get("theme")?.value;

  return (
    <html lang="en">
      <head>
        <script dangerouslySetInnerHTML={{ __html: themeInitScript }} />
      </head>
      <body>
        <StyledComponentsRegistry>
          <ClientThemeProvider
            theme={theme}
            themeDark={themeDark}
            $initial={cookieTheme === "dark" ? "dark" : "light"}
          >
            {children}
          </ClientThemeProvider>
        </StyledComponentsRegistry>
      </body>
    </html>
  );
}

themeInitScript hides the body behind a black (#000) background while it waits, and rewrites the <meta name="theme-color"> tag to the same color, so browser chrome that tints from it (the Safari tab bar, Android Chrome's toolbar) doesn't hold the light server-rendered color until hydration. If your dark theme uses a different page background, build the script with createThemeInitScript instead, so the brief pre-hydration frame matches it:

import { createThemeInitScript } from "cherry-styled-components";

const themeInitScript = createThemeInitScript("#0a0a0f");

The optional second parameter, darkThemeColor, sets the theme-color value the script writes on a dark first visit. It defaults to the background color, which matches the provider's default $themeColor: "light" (covered below). If your provider uses a non-default $themeColor, pass that token's dark value so the pre-hydration tint matches:

import { createThemeInitScript } from "cherry-styled-components";
import { themeDark } from "./theme";

// For a provider with $themeColor="primary"
const themeInitScript = createThemeInitScript(
  themeDark.colors.light,
  themeDark.colors.primary,
);

For the best first visit in Chrome, opt into color-scheme client hints in your middleware so even the very first server render matches the OS preference:

middleware.ts
res.headers.set("Accept-CH", "Sec-CH-Prefers-Color-Scheme");
res.headers.set("Vary", "Sec-CH-Prefers-Color-Scheme");
res.headers.set("Critical-CH", "Sec-CH-Prefers-Color-Scheme");

const hint = req.headers.get("Sec-CH-Prefers-Color-Scheme");
if (!req.cookies.get("theme")?.value && hint) {
res.cookies.set("theme", hint === "dark" ? "dark" : "light", {
  path: "/",
  maxAge: 60 * 60 * 24 * 365,
  sameSite: "lax",
});
}

Server code that needs the active theme object (for example generateViewport for the initial theme-color) can use the resolveTheme helper:

import { resolveTheme } from "cherry-styled-components";
import { theme, themeDark } from "./theme";

const active = resolveTheme(cookieTheme, theme, themeDark);

ClientThemeProvider also keeps a theme-color meta tag in sync with the active theme so browser chrome (like the iOS Safari status bar) matches the page. Point $themeColor at the theme color your page background uses (default "light"), or pass false to disable:

<ClientThemeProvider
  theme={theme}
  themeDark={themeDark}
  $themeColor="tertiaryLight"
>

If your app ships its own global styles, pass $globalStyles={false} and render them as a child so Cherry's defaults don't compete with yours.

In a client-only app there is no server render to flash, so either provider works. To use ClientThemeProvider, resolve the initial theme synchronously (cookie, then localStorage, then matchMedia) before rendering and pass it as $initial.

Both providers expose setTheme and toggleTheme through the same ThemeContext, and both work with the ThemeToggle component. Existing apps using CherryThemeProvider keep working unchanged.