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.
"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 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:
<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:
.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 page, and running the picker inside the
Next.js App Router is covered on the Next.js page.
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.