# Chroma Panel > React color picker with wheel, sliders, palettes, image sampling and an eyedropper. Zero dependencies, accessible, TypeScript types included. # Quick start Source: /react/overview/quick-start ## Installation ```bash npm install chroma-panel ``` There is no CSS import and no provider to set up. ## Anatomy `ColorInput` renders a swatch button that opens the panel in a popover. ```tsx import { ColorInput } from "chroma-panel"; ; ``` ## Controlled Pass `value` and handle `onChange` to keep the color in your own state. Pass `defaultValue` instead and the panel keeps it for you. ```tsx import { useState } from "react"; import { ColorInput, type ColorChangeResult } from "chroma-panel"; export function BrandPicker() { const [color, setColor] = useState("#3366cc"); const handleChange = (result: ColorChangeResult): void => { setColor(result.hex); }; return ; } ``` `onChange` fires continuously while you drag. `onChangeComplete` fires once when you let go — use that one for saving, undo entries and network calls. ## Inline panel `ChromaPanel` is the same panel without the popover, for when you want it on the page. ## Smaller bundle Importing `chroma-panel` registers all five modes. If you only need one or two, import the shell and add the modes yourself. ```tsx import { ChromaPanel } from "chroma-panel/panel"; import "chroma-panel/wheel"; ; ``` That is 14.1 kB instead of 22.2 kB. Every mode has its own entry point — see [entry points](/react/utils/entry-points). ## Next steps - [Theming](/react/handbook/theming) — change how the panel looks - [Controlled and uncontrolled](/react/handbook/controlled) — which props you own - [ColorInput](/react/components/color-input) — the full prop reference --- # Accessibility Source: /react/overview/accessibility You do not need to add anything. Every color axis is a real ``, hidden visually, so keyboard handling and screen-reader support come from the browser rather than a reimplementation of it. ## What you get - Arrows step. Shift+arrow and Page Up/Down step by ten. Home and End jump. - The mode switcher is a tablist with a roving tab stop. - Values are announced as text: "Hue 210 degrees", not a bare number. - The popover traps focus, closes on Escape, and returns focus to the trigger. - Swatches are buttons with accessible names. - `prefers-reduced-motion` and forced-colors mode are both handled. ## Helpers for your own UI ```ts import { readableTextColor, contrastRatio } from "chroma-panel"; readableTextColor("#001f3f"); // '#ffffff' contrastRatio("#fff", "#001f3f"); // WCAG 2.1 ratio ``` `readableTextColor` uses APCA rather than plain luminance. It picks white over mid-blues, where the older method wrongly picks black. More in [Contrast](/react/utils/contrast). ## Data attributes --- # Releases Source: /react/overview/releases ## Canary releases Pre-release versions are published under the `next` tag so they never reach anyone who has not asked for them. Install one by name: ```bash npm install chroma-panel@next ``` They carry a **Pre-release** badge on the timeline above, and are never marked **Latest**. Treat them as a preview: the API in a canary can still change before it lands in a stable release. ## Versioning The package follows semantic versioning. A breaking change to any of the following is a major release: - The props documented under [Components](/react/components/color-input) - The exports of any [entry point](/react/utils/entry-points) - The `--cp-*` custom properties and `data-cp-*` attributes Internal class names that are not listed in the API reference are not part of the public surface and can change in a patch. ## Upgrading ```bash npm install chroma-panel@latest ``` ## Full release notes The timeline above shows the highlights of each release. The complete notes, including every commit and contributor, are on GitHub. - [Releases](https://github.com/re-sohail/chroma-panel/releases) - [Compare any two versions](https://github.com/re-sohail/chroma-panel/compare) --- # About Source: /react/overview/about ## Why the color survives a round trip HSV has two places where information disappears. At zero saturation there is no hue to speak of, and at zero brightness there is neither. Pickers that keep their state as RGB or hex hit this constantly: drag brightness to black and back up, and the hue you picked comes back as red. This one keeps the full HSVA value and merges changes into it instead of replacing it, so dragging to an extreme and back returns the color you started with. `hex` is rounded to 8 bits per channel. Store `hsva` and pass it back to `value` if a color has to survive a round trip unchanged. ## Size Measured as the increase in a real Vite production build, gzipped, with React external. | What you import | Added to your app | | ------------------- | ----------------- | | all five modes | 22.2 kB | | shell plus one mode | 14.1 kB | `dependencies` is empty. `react` and `react-dom` are peer dependencies, so the copy already in your app is the one that gets used. ## Compatibility | | | | ---------------- | ------------------------------------------------------- | | React | 16.14 and newer, including 19 | | React DOM | Required. The popover renders through `createPortal` | | Browsers | Chrome 123, Firefox 120, Safari 17.5 | | TypeScript | Types included, no `@types` package needed | | Modules | ESM and CommonJS | | Server rendering | Safe. Browser-dependent files are marked `'use client'` | See [browser support](/react/handbook/browser-support) for what sets the floor. ## Licence MIT, by [Sohail Khan](https://resohail.me). --- # Theming Source: /react/handbook/theming ```css .cp-root { --cp-accent: #e5484d; --cp-radius-lg: 4px; --cp-width: 280px; } ``` Anything you do not set keeps its default. The panel follows the system color scheme unless you pass `theme`. ## The full set ```css .cp-root { --cp-surface: #ffffff; --cp-surface-raised: #f4f4f6; --cp-border: #d6d6da; --cp-text: #1c1c1e; --cp-text-muted: #6b6b70; --cp-accent: #2d7ff9; --cp-focus: #2d7ff9; --cp-radius-lg: 16px; --cp-radius: 10px; --cp-width: 320px; --cp-disc-size: 196px; --cp-panel-h: 344px; --cp-control-height: 32px; } ``` Two are worth knowing about before you change them. `--cp-control-height` sizes the tab bar, the text inputs, the eyedropper and the footer swatch together, so one value scales every control at once. `--cp-panel-h` is the height set aside for the mode content. It is the same in every mode, which is what stops the panel resizing when you switch tabs. Content taller than it scrolls. Set it to `auto` if you would rather each mode sized itself, and accept that the panel will jump. ## Fitting a container The panel is a fixed 320px wide and reserves a fixed height for its mode content. Both are variables, so it can be made to follow whatever box you put it in — this is what the cards on the home page do. ```tsx ``` `--cp-width: 100%` lets the panel fill its parent. Nothing else has to change: the wheel is sized `min(100%, var(--cp-disc-size))`, so it scales down with the panel rather than overflowing. `--cp-panel-h` is the height set aside for the mode content, and it is the one worth understanding: | Value | What happens | | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | a length, the default | Every mode is that tall, so switching tabs never resizes the panel. A mode with less content than that shows empty space below it. | | `auto` | Each mode is as tall as its own content — no empty space, but the panel resizes when you switch tabs. | Showing a single mode, `auto` is almost always what you want: there is nothing to switch to, so the reserved height only leaves a gap. A length only caps the content when the panel has a height to divide up. With an auto-height panel the content grows past it instead. Give the panel a `height` as well and the mode content scrolls within it — `.cp-panel-host` is already `overflow-y: auto`. ```tsx // Fills its container, and scrolls the palette rather than growing the page. ``` ## If the panel looks wrong The stylesheet lives in `@layer chroma-panel`, so your CSS always wins. That also means CSS you did not aim at the picker wins. Any unlayered element rule in your app beats every rule here, however specific: ```css /* This restyles the picker's swatches and tabs too. */ button { border-radius: 7px; } ``` If the geometry looks off, look for a rule like that first. Scope it, or put your resets in a layer: ```css @layer reset, chroma-panel, utilities; ``` ## Loading the CSS yourself For a strict `style-src` policy, or to extract critical CSS: ```tsx import "chroma-panel/styles.css"; ; ``` Injection is keyed on `getRootNode()`. The picker works inside a shadow root or an iframe, and many panels still produce one `