# Dark Mode

> Enable dark mode in Cherry with automatic detection, manual toggle support, and flash-free server-side rendering.

Source: https://cherry.al/code/dark-mode

> For the complete documentation index, see [llms.txt](https://cherry.al/llms.txt).

# Dark Mode

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

```tsx
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](/code/theme-toggle) component. It calls `toggleTheme()` from `ThemeContext` and persists the choice automatically:

```tsx
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, <kbd>Command</kbd> + <kbd>Shift</kbd> + <kbd>L</kbd> (<kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>L</kbd> on Windows) flips the theme, using the same `toggleTheme` and persistence as a click:

```tsx
<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:

```tsx
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.

```mermaid
sequenceDiagram
  participant Browser
  participant Server
  participant Provider as ClientThemeProvider
  Browser->>Server: Request page with the theme cookie, if set
  Server-->>Browser: HTML rendered in the right theme ($initial from the cookie)
  Note over Browser: First visit without a cookie? themeInitScript seeds it from the OS preference and hides the body until the theme is applied
  Browser->>Provider: Mount
  Provider->>Provider: Reconcile the cookie and OS preference
  Provider-->>Browser: Swap the theme in place only if the first paint was wrong
```

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](/code/installation):

<Code title="app/layout.tsx" language="tsx" code={`// 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:

```tsx
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:

```tsx
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:

<Code title="middleware.ts" language="ts" code={`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:

```ts
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:

```html
<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`.

<Callout type="note">
  Both providers expose `setTheme` and `toggleTheme` through the same
  `ThemeContext`, and both work with the `ThemeToggle` component. Existing apps
  using `CherryThemeProvider` keep working unchanged.
</Callout>

<Button href="https://github.com/cherry-design-system/styled-components/blob/main/src/lib/styled-components/client-theme-provider.tsx" icon="code" iconPosition="left">
  View Source
</Button>
