Skip to content

TheAleSch/amplo-picker

Repository files navigation

Amplo Fill Picker

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.json

Replace the URL with your deployed origin if you fork this repo. The registry artifact is generated by pnpm registry:build and served from public/r/.

Live demo · Docs · Playground


Why

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 in display-p3 color space; out-of-sRGB swatches and previews paint in their native gamut on capable displays. The default output format is p3.
  • 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 via next/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).

Quick start

"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.


API

<ColorPicker.Root>

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".

Parts

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.

Hook

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
});

Color utilities

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";

Color spaces — quick primer

  • 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.


Accessibility

  • 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" with aria-roledescription and aria-valuetext describing the current point.
  • Pointer + touch — pointer capture so drags don't escape the slider/area; touch-none to 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.

Saving user swatches

<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]))
    }}
  />
);

Publishing your own registry

This repo is set up as a third-party shadcn registry. To publish your own fork:

  1. Edit registry.json — change name, homepage, item names. The picker declares ["utils", "tooltip"] as registryDependencies, so installing it pulls shadcn's tooltip (and its radix dep) automatically.
  2. Run pnpm registry:build to emit public/r/*.json.
  3. Deploy the Next.js app (Vercel works out of the box).
  4. 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.


Development

Package manager is pnpm.

  • pnpm dev — Next.js dev server (Turbopack). Runs the demo / docs site.
  • pnpm build — Next.js production build.
  • pnpm lintnext lint.
  • pnpm typechecktsc --noEmit.
  • pnpm test — Vitest, single run. (pnpm test:watch for watch mode.)
  • pnpm registry:build — regenerate public/r/<item>.json from registry.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. The new-york segment is the shadcn style identifier.

License

MIT © Alexandre Schrammel

About

wide color gamut color picker for React

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages