# Dark mode

> Run the React color picker in a dark interface: follow the system by default, force one theme, or match the class-based toggle your app already has.

Source: https://chroma-panel.jscrate.dev/react/recipes/dark-mode
Last updated: 2026-09-18

Out of the box the picker follows the operating system, which is right until
your app has a theme toggle of its own. Then the two disagree: someone reading
your site in dark mode on a light desktop opens the picker and gets a white
panel. Hand the picker your resolved theme and the mismatch goes away.

```tsx
"use client";

import { ColorInput } from "chroma-panel";
import "chroma-panel/style.css";
import { useTheme } from "next-themes";

function panelTheme(value: string | undefined) {
  if (value === "dark") return "dark" as const;
  if (value === "light") return "light" as const;
  return undefined;
}

export function ThemedColorInput() {
  const { resolvedTheme } = useTheme();

  return (
    <ColorInput
      name="brand"
      defaultValue="#3366cc"
      theme={panelTheme(resolvedTheme)}
    />
  );
}
```

`theme` accepts `"dark"` and `"light"` and nothing else, while `resolvedTheme`
is typed `string | undefined`, so the narrowing above is not ceremony. Returning
`undefined` is a real answer too: leaving `theme` off puts the panel back on the
system preference, which is the best guess while your provider has not resolved
yet.

The same prop is on `ChromaPanel`, and works the same way on both — see the
[ChromaPanel reference](https://chroma-panel.jscrate.dev/react/components/chroma-panel) for where it sits among
the other panel props.

## Following the system

This is the default. The panel's surfaces, borders, text and accent are all
declared as `light-dark()` pairs, and the panel roots declare
`color-scheme: light dark`, so the browser picks the side that matches the
reader's preference. Nothing to pass, nothing to
hydrate, and no flash on first paint, because the choice is made in CSS rather
than in JavaScript.

## Forcing one theme

Pass `theme` directly when the surrounding surface is fixed. A picker in a dark
editor chrome should stay dark even in a light app:

```tsx
<ChromaPanel theme="dark" defaultValue="#3366cc" />
```

## How the stylesheet decides

`theme` lands on the panel root as `data-cp-theme`, with the value `dark` or
`light`. The stylesheet then sets `color-scheme` on that root to the single
value, which is what makes every `light-dark()` token resolve to that side. Pass
nothing and there is no attribute, so `color-scheme: light dark` stands and the
preference decides.

The bottom sheet that the picker becomes below 640px is matched separately, so
its backdrop and surface follow the panel inside it rather than going their own
way.

Because the attribute sits on the panel root itself, it travels with the panel
into the popover, which renders in `document.body` rather than inside your
layout. Dark styling you scope to a wrapper element will not reach it; a
selector on `.cp-root` will.

## Changing the dark palette

The package's rules live in a `@layer chroma-panel` cascade layer, and unlayered
CSS beats a layer. Your own stylesheet can therefore replace any token without
fighting specificity:

```css
.cp-root {
  --cp-surface: light-dark(#ffffff, #0b0b0f);
  --cp-accent: light-dark(#2d7ff9, #7aa2ff);
}
```

Writing the override as a `light-dark()` pair keeps both sides in one rule and
keeps the automatic behavior working. The full list of tokens is on the
[theming](https://chroma-panel.jscrate.dev/react/handbook/theming) page, and running the picker inside the
Next.js App Router is covered on the [Next.js page](https://chroma-panel.jscrate.dev/react/frameworks/next-js).

> **Two ways to be dark**
>
> `theme` changes the panel only. The `--cp-` custom properties change what the
> panel's colors are. Reach for `theme` to pick a side, and for the properties
> when the built-in dark side is not the dark you want.
