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:
- The server reads the
themecookie and passes the verdict as$initial, so the first paint is already correct. 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) and hides the body until the client has applied the right theme.- On mount, the provider reconciles the server's guess against the cookie and OS preference and swaps the theme in place if needed.
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:
// Server component
import { cookies } from "next/headers";
import {
ClientThemeProvider,
StyledComponentsRegistry,
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. 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");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:
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.