Inputs
Color Picker
A hex-driven color picker — saturation/value area, hue and alpha sliders, a format-aware text field, quick swatches, and an optional native eyedropper.
Preview in your theme
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/color-picker.jsonPrompt
The prompt behind this component — paste it into your AI assistant to recreate or adapt it.
Build a React + TypeScript + Tailwind "ColorPicker" component (lucide-react
Check/Copy/CopyX/Pipette icons, no color-math libraries — HSV math is
hand-written).
Contract
- Export a forwardRef div. Props: value (string, controlled hex — `#rgb`,
`#rrggbb`, or `#rrggbbaa`), onValueChange ((hex: string) => void), format
("hex" | "rgb" | "hsl", default "hex" — only affects the text field's
display/parse echo, the value contract itself is always hex), swatches
(string[] of hex colors, sensible 8-color default, pass [] to hide the
row), alpha (boolean, default false — shows the opacity slider and allows
alpha in the emitted hex/rgb()/hsl() text), disabled (default false),
label (accessible name for trigger + panel, default "Color", not rendered
visibly), showEyedropper (boolean, default true, gated by capability
detection — see below).
Behavior
- Trigger is a button showing a small checkerboard-backed swatch (so partial
alpha is visible) plus the current value formatted per `format`. Clicking
toggles an absolutely-positioned panel below it (role="dialog",
aria-label). Outside pointerdown and Escape close it (Escape also returns
focus to the trigger); listeners are added only while open and removed on
close/unmount.
- Panel contains, top to bottom: a 2D saturation/value area (role="slider"
on its thumb, aria-valuenow = saturation%, aria-valuetext describing both
saturation and brightness; arrow keys nudge by 1, Shift+arrow by 10, no
Home/End), a hue slider (0-360, role="slider", arrow keys ±1/±15 with
Shift, Home/End jump to 0/360), an alpha slider shown only when `alpha` is
true (0-100%, same keyboard scheme, track is a checkerboard with a
transparent→opaque gradient of the current color on top), a text input, a
copy button, an optional eyedropper button, and a swatch row.
- 2D area and both sliders use pointer events + setPointerCapture, with
strict pointerId isolation: a shared "which control, which pointerId is
dragging" ref means a second touch point landing on the same or a
different control while one drag is in flight is ignored outright — only
the pointer that started the drag can move or end it.
- Internally the color lives as HSV (h 0-360, s/v 0-100) plus alpha 0-1 —
the natural representation for the 2D area + hue slider. It's re-derived
from the `value` hex prop whenever that prop actually changes (not on
every render), with one guard: if the newly-parsed color is pure black
(v=0) or fully desaturated gray (s=0), the previous hue/saturation is kept
instead of the arithmetic h=0 fallback, so dragging into a gray/black
corner and back out doesn't snap the hue slider to red.
- Text field accepts hex, rgb()/rgba(), or hsl()/hsla() regardless of
`format` — any of the three that parses is emitted immediately as you
type. On blur, if the current text doesn't parse, it reverts to the
last-committed value (reformatted per `format`) and sets aria-invalid;
focusing the field again clears aria-invalid. It never throws on garbage
input.
- Swatch click and a successful eyedropper pick both call the same commit
path as dragging. Eyedropper button only renders when `showEyedropper` is
true AND the browser exposes `window.EyeDropper` — detected once via
useSyncExternalStore (never read directly during render), so server and
first client render agree and there's no hydration mismatch.
- Copy button copies the current formatted string via the Clipboard API,
shows a checkmark for 2s on success; if the API is unavailable or the
write rejects, shows a distinct "failed" icon/aria-label instead of
silently doing nothing.
- disabled: real HTML disabled on the trigger button (nothing downstream
needs its own disabled branches since the panel can never open); if
disabled flips true while the panel happens to be open, it force-closes.
Rendering & styling
- Chrome (borders, panel background, focus rings, text) uses semantic
tokens only: border, bg-popover/bg-background, text-foreground,
text-muted-foreground, ring-ring, border-destructive for the invalid
state. The checkerboard (transparency chrome) is also token-based via
repeating-conic-gradient + color-mix(in oklab, var(--muted-foreground)
35%, transparent) — never a raw hex.
- The actual color swatches, the saturation-value area's hue-tinted
background, the hue-spectrum gradient, and the alpha gradient are runtime
color *data* (from props/state), so they're painted via inline style and
are explicitly exempt from the token rule — same exemption this repo's
Swatch Picker documents for its check mark.
- cn() merges className on the root. motion-reduce disables the swatch
hover scale.
Customization levers
- Panel width/placement: change the `w-64`/`mt-2` panel classes, or swap the
manual absolute-position panel for a floating-ui/Radix Popover if you need
collision-aware placement.
- 2D area size/shape: aspect-square is a deliberate choice; make it a fixed
height rectangle instead if you want a shorter panel.
- Default swatches: swap DEFAULT_SWATCHES for your brand palette.
- Alpha: flip the default to true if your product edits colors that always
need opacity control.
- Add a "recent colors" row by lifting a small history array in the
consumer and passing it through `swatches` alongside (or instead of) the
fixed palette.Concepts
- HSV decode/re-encode — the picker's only source of truth is the
valuehex prop; internally it decodes to hue/saturation/value + alpha because that maps directly onto the 2D area and hue slider, then re-encodes to hex on every change. - Gray/black hue preservation — hue is mathematically undefined at zero saturation or zero value; the decode step keeps the previous hue/saturation in those cases so the hue slider doesn't visibly snap to red when you drag into a corner and back out.
- pointerId isolation — each draggable (2D area, hue slider, alpha slider) tracks which single pointerId started its gesture; any other pointer's move/up events on that same control are ignored, so a second touch point can't hijack or prematurely end an in-progress drag.
- Format is display-only —
formatchanges how the text field reads and what syntax it echoes back; thevaluecontract passed toonValueChangeis always hex, regardless offormat. - Text field rollback — typing is validated on every keystroke (hex,
rgb(), orhsl()— whichever parses commits immediately); only on blur does an unparseable string get reverted, paired witharia-invalidso screen readers and sighted users both get a signal. - Capability-detected eyedropper —
window.EyeDropperis an experimental, not-universally-supported API; the button is present only when the browser exposes it, checked once viauseSyncExternalStoreso the server-rendered markup and the first client render never disagree.
Time Picker
A segmented hour:minute input — type digits or arrow-step, plus a clock-button dropdown of minuteStep candidates — always normalized to 24-hour "HH:mm" regardless of display format.
Prompt Input
An autosizing chat input with attachments, streaming stop/send, IME-safe submit and a character counter.