Skip to documentation content

Color Picker

Choose a precise color, opacity, and output format.

color-picker-demo

Overview

ColorPicker is a precision tool for people who need an exact color — theme builders, design settings, data-viz configuration, brand customization. It works internally in OKLCH, a perceptual color space where "a bit lighter" is the same size change everywhere on the gradient, and outputs hex, HSL, RGB, or OKLCH strings.

Use it when the person must get a specific value right. For choosing among known options (plan colors, status tints), render swatches as buttons instead — free-form picking is more power, and more surface for mistakes, than that job needs.

Anatomy

The picker composes from parts, so you can ship exactly the control each context needs:

  • ColorPicker — the root; owns value, mode, and clamping.
  • ColorPickerArea — the 2-D gradient field (lightness × chroma in OKLCH mode, saturation × lightness in HSL mode).
  • ColorPickerHue — hue slider.
  • ColorPickerAlpha — transparency slider.
  • ColorPickerInput — text entry for the current output string.
  • ColorPickerMode — format switcher between hex, hsl, rgb, and oklch.

Behavior

Perceptual space. The area operates in OKLCH by default. Moving diagonally changes perceived lightness uniformly — unlike HSL fields, where dragging through yellow suddenly looks far brighter. Switch to HSL when someone needs to think in the classic saturation/lightness model.

Gamut honesty. OKLCH can describe colors wider-gamut displays can show but ordinary sRGB screens cannot. The component keeps your raw OKLCH value intact and only displays the sRGB-clamped version. What you store is what you asked for; what you see is what fits the screen. If exact on-screen reproduction matters, read back the displayed value rather than assuming identity.

Modes. The output string follows the active mode, and switching modes converts without drift. Alpha is included where the format supports it (#RRGGBBAA, hsla(), rgba(), oklch(… / α)).

Keyboard interaction

The area, hue, and alpha are slider primitives:

KeyResult
ArrowLeft / ArrowDownDecrease the controlled axis
ArrowRight / ArrowUpIncrease it

Each arrow step moves the area by a fixed perceptual increment, so keyboard adjustment is usable rather than a crawl; there is no large-step modifier. For exact values or big jumps, type into ColorPickerInput — parsing accepts any CSS color string culori understands.

Accessibility

Every part is reachable with Tab and operable with arrows; sliders expose their current axis value to screen readers via the underlying slider role. The text input is the precision path — always include it. A picker used without any textual representation excludes people who cannot discriminate fine color differences, and makes matching an existing brand value impossible.

Never let the picked color alone carry meaning. A chosen red means nothing without adjacent text saying "danger" or "error"; color-vision deficiencies and forced-colors mode strip exactly that channel.

All surfaces come from tokens except the color being edited itself, which is inherently user content — check that surrounding labels stay legible against both themes. Layout mirrors in RTL because the parts stack with logical spacing; slider arrows keep their geometric meaning rather than flipping, which matches how color axes are conventionally drawn.

Installation

npx honestui@latest add color-picker

Usage

import {
  ColorPicker,
  ColorPickerAlpha,
  ColorPickerArea,
  ColorPickerHue,
  ColorPickerInput,
  ColorPickerMode,
} from "@/components/ui/color-picker";
<ColorPicker defaultValue="#7C3AED">
  <ColorPickerArea />
  <ColorPickerHue />
  <ColorPickerAlpha />
  <div className="flex gap-2">
    <ColorPickerInput />
    <ColorPickerMode />
  </div>
</ColorPicker>

The value is a controlled or uncontrolled string in the active mode's format.

Don't do this

Swatch-only pickers with no way to type

// Bad
<ColorPicker>
  <ColorPickerArea />
  <ColorPickerHue />
</ColorPicker>
// Good
<ColorPicker>
  <ColorPickerArea />
  <ColorPickerHue />
  <ColorPickerInput />
</ColorPicker>

Without text entry there is no way to enter #0F52BA exactly, match a value from another tool, or adjust color at all without fine visual-motor control. The input is not an extra — it is the accessible, precise path, and drag-only interfaces fail both screen-reader users and anyone who knows the value they want.

Letting the picked color carry the meaning

// Bad
<div style={{ background: picked }} aria-label={picked} />
// Good
<span className="inline-flex items-center gap-2">
  <span style={{ background: picked }} aria-hidden="true" className="size-4 rounded-full border" />
  {colorName ?? picked}
</span>

A color chip labeled only with its own hex value announces gibberish ("four two four two four two") and conveys nothing to color-blind readers. Whatever the color signifies — category, status, team — say it in text next to the swatch.

Letting picked colors pick unreadable text

// Bad
<div style={{ background: picked }}>
  <span>Save</span> // white text on a lemon-yellow pick
</div>
// Good
const foreground = isDark(picked) ? "#FFFFFF" : "#1A1A1A";

<div style={{ background: picked, color: foreground }}>
  <span>Save</span>
</div>

Once a person can choose any background, text placed on it inherits every possible contrast failure. Compute the foreground from the chosen value's lightness instead of hard-coding it — OKLCH lightness makes that check a threshold, not a judgment call.

Examples

Controlled with mode switching

color-picker-controlled

Full picker

color-picker-demo

API reference

PartPropType
ColorPickervalue / defaultValuestring (any supported format)
ColorPickeronValueChange(value: string, mode: ModeType) => void
ColorPickermode / defaultMode"hex" | "hsl" | "rgb" | "oklch"
ColorPickerModeonModeChange(mode: ModeType) => void

Out-of-sRGB-range OKLCH values round-trip unchanged; the rendered preview shows the clamped equivalent. Parsing uses culori, so any valid CSS color string is accepted as input.