OKLCH-native, Display-P3-aware fill picker for shadcn/ui. Color today; gradients, images, and video on the way. Composable, accessible, gamut-aware. Drop into any Next.js + Tailwind v4 app with one CLI command.
pnpm dlx shadcn@latest add https://amplo.ale.design/r/fill-picker.jsonReplace the URL with your deployed origin if you fork this repo. The registry artifact is generated by
pnpm registry:buildand served frompublic/r/.
Live demo · Docs · Playground
Most color pickers in the React ecosystem are sRGB-only. They can't represent the wider gamut of modern displays (Display-P3 covers ~50% more colors), they don't surface contrast metrics for accessibility work, and they don't warn users when their chosen color falls outside what an sRGB monitor can show. This component fixes all three.
- OKLCH-canonical — all color state is stored as OKLCH so format toggles (hex / rgb / hsl / hsb / oklch / oklab / display-p3) are lossless round-trips.
- Display-P3 rendering — when the device reports
(color-gamut: p3), the saturation canvas is painted indisplay-p3color space; out-of-sRGB swatches and previews paint in their native gamut on capable displays. The default output format isp3. - Stretched-gamut fill + warning lines — the 2D Area always fills with in-gamut color: the active render gamut is stretched to the square's edges, and narrower-gamut cutoffs are drawn as thin warning lines inside the fill. Render gamut tracks the output format (hex/rgb/hsl/hsb → sRGB, p3 → P3, oklch/oklab → Rec.2020) and is overridable per
<ColorPicker.Area gamut="..." />. Live<GamutBadge>confirms where the picked color lands, with a hover tooltip. - Live contrast metrics with rich popover — WCAG 2.1 ratio + AA/AAA badges and APCA Lc + body/headline badges, surfaced one at a time. Hovering the readout opens a popover that shows every threshold (with a green ✓ or red ✗ icon) and the foreground/background pair being tested. When more than one metric is enabled the readout becomes a clickable toggle to flip between WCAG and APCA.
- Mode-aware hue slider — when the active format is HSL or HSB the slider tracks that format's hue scale (so the bead matches the channel input H exactly); for OKLCH/OKLab it tracks canonical OKLCH hue with chroma rescaling on commit; hex/RGB/P3 fall back to OKLCH hue.
- Compose-only API — Radix-style compound parts. There is no kitchen-sink default component; you build the layout you need.
- shadcn-native styling — uses semantic tokens (
bg-popover,border-input,ring-ring, etc.), Geist Sans + Mono vianext/font, identical input/select/button visuals to the rest of your shadcn project. - Accessible — full keyboard control on every part (arrow keys with shift modifier, Home/End, PageUp/PageDown), proper
aria-valuetext, focus rings, touch-friendly pointer capture, color-independent status (text + icons, not just hue).
"use client";
import * as React from "react";
import { ColorPicker, parseColor } from "@/components/ui/fill-picker/fill-picker";
export function Example() {
const [color, setColor] = React.useState(() => parseColor("oklch(0.7 0.18 30)")!);
return (
<ColorPicker.Root
value={color}
onValueChange={setColor}
backgroundColor="#ffffff"
>
<div className="flex items-stretch gap-2">
<ColorPicker.GamutBadge showLabel={false} className="w-auto flex-1 justify-center" />
<ColorPicker.ContrastReadout
metrics={["wcag", "apca"]}
showLabel={false}
showValue={false}
className="w-auto flex-1 justify-center"
/>
</div>
<ColorPicker.Area mode="oklch-cl" />
<div className="flex flex-col gap-1.5">
<ColorPicker.Hue />
<ColorPicker.Alpha />
</div>
<div className="flex items-center gap-2">
<ColorPicker.FormatSwitcher className="flex-1" />
<ColorPicker.EyeDropper className="h-8 w-full flex-1" />
</div>
<ColorPicker.ChannelInput showFormat={false} />
<ColorPicker.Swatches />
</ColorPicker.Root>
);
}The playground ships eight named variants (Canonical, Compact, Minimal, Sliders only, Area only, Framer, Figma, A11y review, Brand swatches) and emits the JSX for each layout — copy-paste from there.
| Prop | Type | Default | Description |
|---|---|---|---|
value |
string | OklchColor |
— | Controlled value. Any CSS Color 4 string or canonical OKLCH object. |
defaultValue |
string | OklchColor |
— | Uncontrolled initial value. |
onValueChange |
(color: OklchColor, formatted: string, formats: Record<ColorFormat, string>) => void |
— | Fires on every change. formats includes a pre-serialized string in every format. |
format |
ColorFormat |
— | Controlled output format. |
defaultFormat |
ColorFormat |
"p3" |
Uncontrolled initial format. |
onFormatChange |
(format: ColorFormat) => void |
— | Fires on format toggle. |
formats |
ColorFormat[] |
all | Restricts which output formats the picker exposes (FormatSwitcher options + the resolved default). |
backgroundColor |
string | OklchColor |
"#fff" |
Background used for contrast metrics + Preview composite. |
ColorFormat = "hex" \| "rgb" \| "hsl" \| "hsb" \| "oklch" \| "oklab" \| "p3".
| Part | Notable props | Notes |
|---|---|---|
<ColorPicker.Area> |
mode, gamut, chromaMax, showWarningLines |
2D canvas. mode = "oklch-cl" (perceptually uniform: Y = OKLCH lightness, top row white, max-saturation at the gamut cusp), "hsv-sv" (HSV-style: top-left white, top-right fully saturated, like Photoshop / Framer), "oklch-hc" (hue × chroma; pair with <ColorPicker.Lightness>). All modes always fill the square with in-gamut color; narrower-gamut cutoffs render as thin SVG warning lines. Defaults to h-45 w-full (180px tall, fills picker width). Keyboard: arrows ±1%, Shift+arrows ±10%, Home/End, PgUp/PgDn. |
<ColorPicker.Hue> |
orientation |
Hue slider. Mode-aware (see above). |
<ColorPicker.Lightness> |
orientation |
Lightness slider — pair with mode="oklch-hc" Area. |
<ColorPicker.Alpha> |
orientation |
Opacity slider with checkerboard. |
<ColorPicker.Preview> |
— | 40px swatch composited over backgroundColor. |
<ColorPicker.CssInput> |
— | Single text input. Parses any CSS Color 4 string on Enter/blur, marks invalid via aria-invalid. Escape reverts. |
<ColorPicker.FormatSwitcher> |
formats |
Native <select> of formats — pass formats to override the list locally. |
<ColorPicker.ChannelInput> |
formats, showFormat |
Photoshop-style multi-field input: format selector + one numeric field per channel + alpha %. Set showFormat={false} to drop the inline selector when pairing with a standalone <FormatSwitcher>. Each numeric field supports ↑/↓ to step (Shift = big step) and accepts a pasted CSS color string. |
<ColorPicker.Swatches> |
presets, onAdd |
Grid of preset chips. presets accepts any CSS color strings (including wide-gamut color(display-p3 …) — they paint in their native gamut on capable displays). When onAdd is provided, renders a "+" tile that calls onAdd(color, hex); the consumer owns persistence (lift presets and update on add — localStorage / a server / a store / etc.). Transparent presets show a checkerboard underlay. |
<ColorPicker.GamutBadge> |
showLabel |
Live status: sRGB / P3 / Rec.2020 / Out of gamut. Hover tooltip names the active color space. showLabel={false} drops the "Gamut" prefix. |
<ColorPicker.ContrastReadout> |
metrics, defaultMetric, showLabel, showValue, showBadges |
Surfaces one contrast metric at a time. metrics: ("wcag" | "apca")[] defaults to ["wcag"]; when length > 1 the readout becomes a button that cycles. Hover opens a popover listing every threshold with ✓ / ✗ icons + the foreground/background pair being tested. Toggle showLabel / showValue / showBadges to slim down the inline display — set everything but showBadges to false for a minimal pass/fail badge. |
<ColorPicker.EyeDropper> |
— | Native EyeDropper API. Renders nothing on unsupported browsers. |
useColorPicker(props) powers the headless layer. Useful when you want a totally custom UI but the same state machine.
const {
color, // canonical OklchColor
format,
formatted, // string in `format`
formats, // ColorFormat[] — the list of allowed output formats
formatStrings, // Record<ColorFormat, string> — every format pre-serialized
gamut, // GamutInfo
contrast, // { wcag, wcagLevel, apca }
setColor, // accepts string | OklchColor
setComponent, // ('l'|'c'|'h'|'alpha', value) — clamped
adjustComponent, // ('l'|'c'|'h'|'alpha', delta) — wraps for hue
setFormat,
setFromString, // (s) => boolean; false on parse failure
background,
} = useColorPicker({
defaultValue: "#ff0000",
defaultFormat: "p3",
backgroundColor: "#fff",
formats: ["hex", "oklch", "p3"], // optional; defaults to all
});import {
parseColor, // (string) => OklchColor | null
formatColor, // (OklchColor, ColorFormat) => string (sRGB/P3 outputs are gamut-mapped first; hex is uppercased)
formatAll, // (OklchColor) => Record<ColorFormat, string>
gamutInfo, // (OklchColor) => { inSrgb, inP3, inRec2020 }
toGamut, // (OklchColor, "srgb"|"p3"|"rec2020") => OklchColor (CSS Color 4 chroma reduction)
contrast, // (fg, bg) => { wcag, wcagLevel, apca }
apcaContrast, // (fg, bg) => Lc number
isValidColor, // (string) => boolean
} from "@/components/ui/fill-picker/fill-picker";- sRGB — the baseline web gamut. Every device made in the last 30 years can render it. Hex /
rgb()/hsl()all live here. - Display-P3 — Apple's wide-gamut space. Every recent iPhone, iPad, and MacBook supports it; many newer Android phones too. About 25% wider than sRGB, especially in reds and greens. Use
color(display-p3 r g b)syntax. - OKLCH — perceptually uniform polar space. The same chroma value looks equally vivid across all hues, and the same lightness looks equally bright. This is why all picker state is stored here — sliders feel intuitive and conversions don't drift. CSS Color 4:
oklch(L C H). - OKLab — same color space as OKLCH but in cartesian (a/b) form. Good for color-difference math, less good for UIs.
When you author a P3 or wider color and the user's display can't render it, the browser falls back. <GamutBadge> and the warning lines on <Area> keep you informed: in p3 mode the area fills with P3 colors and a single thin line marks the sRGB cutoff; in oklch/oklab mode the area fills up to Rec.2020 and two thin lines mark the sRGB and P3 cutoffs. In sRGB-targeted formats (hex/rgb/hsl/hsb) the area fills with sRGB only — no warning line is needed because every visible pixel is also an sRGB color.
- Keyboard — every interactive part is reachable via Tab. Sliders follow the WAI-ARIA APG slider pattern (arrow keys ±1, Shift ±10, Home/End, PageUp/Down). The 2D Area uses
role="application"witharia-roledescriptionandaria-valuetextdescribing the current point. - Pointer + touch — pointer capture so drags don't escape the slider/area;
touch-noneto suppress browser scroll while interacting. - Focus — visible focus ring on all controls via
focus-visible:ring. - Color independence — gamut and contrast information is conveyed via text + icons + ARIA, not color alone. Pass/fail badges have hover tooltips and accessible names ("AA passes" / "AA fails").
- Reduced motion — no auto-animation; only a swatch hover scale that respects user preference.
<ColorPicker.Swatches> keeps persistence the consumer's job — pass onAdd and lift the presets array.
const STARTERS = ["#ffffff", "#000000", "oklch(0.7 0.18 30)"];
const [saved, setSaved] = React.useState<string[]>([]);
return (
<ColorPicker.Swatches
presets={[...STARTERS, ...saved]}
onAdd={(_color, hex) => {
setSaved((prev) => prev.includes(hex) ? prev : [...prev, hex]);
// Or hit your server: fetch("/api/swatches", { method: "POST", body: JSON.stringify({ hex }) })
// Or persist locally: localStorage.setItem("swatches", JSON.stringify([...saved, hex]))
}}
/>
);This repo is set up as a third-party shadcn registry. To publish your own fork:
- Edit
registry.json— changename,homepage, item names. The picker declares["utils", "tooltip"]asregistryDependencies, so installing it pulls shadcn'stooltip(and its radix dep) automatically. - Run
pnpm registry:buildto emitpublic/r/*.json. - Deploy the Next.js app (Vercel works out of the box).
- Users install with
pnpm dlx shadcn@latest add https://<your-host>/r/color-picker.json.
The registry build is incremental and ships only what's listed in the manifest — keep registry.json as the source of truth. Outputs in public/r/*.json are gitignored — they're produced as a build artifact for deployment.
Package manager is pnpm.
pnpm dev— Next.js dev server (Turbopack). Runs the demo / docs site.pnpm build— Next.js production build.pnpm lint—next lint.pnpm typecheck—tsc --noEmit.pnpm test— Vitest, single run. (pnpm test:watchfor watch mode.)pnpm registry:build— regeneratepublic/r/<item>.jsonfromregistry.json.
Source roots:
src/— the demo / docs Next.js app (App Router). Not shipped to consumers.registry/new-york/color-picker/— the actual component source bundled into the registry artifact. Thenew-yorksegment is the shadcn style identifier.
MIT © Alexandre Schrammel