Chroma Panel

Search documentation

Find a page or section

Works in both routers with no dynamic import.

chroma-panel is a React color picker for Next.js that needs no wrapper and no configuration. Every file in the package that touches the browser already carries "use client", so importing it from a server component is fine. This page sets it up in the App Router and in the Pages Router.

Install

npm install chroma-panel

There are no other packages to add. dependencies is empty, and react and react-dom are peer dependencies, so the copies already in your app are the ones that get used.

The stylesheet

The panel injects its stylesheet from an effect. That means server-rendered markup is unstyled until hydration. To avoid that flash, import the stylesheet yourself and turn injection off:

<ColorInput injectStyles={false} />

Where the import goes depends on the router, so each section below shows it. injectStyles={false} only stops the <style> tag being written; the CSS is in your bundle either way. If you use Tailwind, import the stylesheet from your CSS file instead and declare the layer order, as styling with Tailwind explains.

Setting up a page

App Router

Import the stylesheet once in your root layout. The layout stays a server component.

// app/layout.tsx
import "chroma-panel/style.css";
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

A server component can render the picker directly, as long as every prop you pass is serializable:

// app/brand/page.tsx
import { ColorInput } from "chroma-panel";
 
export default function BrandPage() {
  return (
    <main>
      <h2>Brand color</h2>
      <ColorInput defaultValue="#3366cc" injectStyles={false} />
    </main>
  );
}

The moment you hold the color yourself, the file becomes a client component. useState and an onChange handler are both client-only, so put them behind your own "use client":

// app/brand/brand-picker.tsx
"use client";
 
import { useState } from "react";
import { ColorInput, type ColorChangeResult } from "chroma-panel";
 
export function BrandPicker() {
  const [color, setColor] = useState<string>("#3366cc");
 
  const handleChange = (result: ColorChangeResult): void => {
    setColor(result.hex);
  };
 
  return (
    <>
      <ColorInput value={color} onChange={handleChange} injectStyles={false} />
      <p style={{ color }}>{color}</p>
    </>
  );
}

That file is a client component because of your state, not because of chroma-panel. Which props you own and which the panel keeps is covered in controlled and uncontrolled.

Pages Router

The Pages Router has no server components, so "use client" does not apply anywhere. Next.js only accepts a global stylesheet import in pages/_app.tsx, so that is where the CSS goes:

// pages/_app.tsx
import type { AppProps } from "next/app";
 
import "chroma-panel/style.css";
 
export default function App({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />;
}

The page itself is an ordinary React component:

// pages/brand.tsx
import { useState } from "react";
import { ColorInput, type ColorChangeResult } from "chroma-panel";
 
export default function BrandPage() {
  const [color, setColor] = useState<string>("#3366cc");
 
  const handleChange = (result: ColorChangeResult): void => {
    setColor(result.hex);
  };
 
  return (
    <main>
      <ColorInput value={color} onChange={handleChange} injectStyles={false} />
    </main>
  );
}

Where "use client" is needed

FileNeeds "use client"
A server component that renders the pickerNo
A file that holds the color in useStateYes
A file that passes onChangeYes
Anything in pages/Not applicable

The rule is about your own code. A server component cannot pass a function across the boundary, so a handler means a client component.

No dynamic import needed

You do not need next/dynamic, and you do not need ssr: false. Nothing is read from window at module scope, injectStyles returns early when document is undefined, and the first render writes the color as CSS custom properties, so the server-rendered markup already shows the right color.

Do not reach for ssr: false

Wrapping the picker in a dynamic import with ssr: false only delays it by a round trip. It renders on the server correctly as it is.

Server rendering covers the same ground for Remix and other SSR setups, and entry points lists the subpaths if you want a smaller bundle than the default import.