Color Picker

Popover color picker with swatches, a hue slider, and a hex input. Install Color Picker from the VLLNT UI registry with the shadcn CLI.

Report a bug

Popover color picker with swatches, a hue slider, and a hex input. Part of the form family in VLLNT UI, it ships as a machine-readable registry entry — copy the source directly into your app with the shadcn CLI and own it, no runtime dependency on a component library.

Preview

Switch between light and dark to inspect the embedded Storybook preview.

Installation

Add Color Picker to your project with the shadcn CLI. The source lands in your codebase, ready to adapt:

pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/color-picker.json

Source

"use client"; import * as React from "react"; import { cn } from "../../lib/utils"; import { Button } from "../button/button"; import { Input } from "../input/input"; import { Popover, PopoverContent, PopoverTrigger } from "../popover"; const presetSwatches: string[] = [ "#ef4444", "#f97316", "#f59e0b", "#84cc16", "#22c55e", "#14b8a6", "#3b82f6", "#6366f1", "#a855f7", "#ec4899", "#64748b", "#0f172a", ]; function hexPart(value: number): string { return value.toString(16).padStart(2, "0"); } function toHex(red: number, green: number, blue: number): string { return `#${hexPart(red)}${hexPart(green)}${hexPart(blue)}`; } function hueChannel(hue: number, offset: number): number { const k = (offset + hue / 30) % 12; const amount = 0.45; const value = 0.5 - amount * Math.max(-1, Math.min(k - 3, 9 - k, 1)); return Math.round(value * 255); } function hueToHex(hue: number): string { return toHex(hueChannel(hue, 0), hueChannel(hue, 8), hueChannel(hue, 4)); } type SwatchGridProps = { onSelect: (value: string) => void; selected: string; swatches: string[]; }; function SwatchGrid({ onSelect, selected, swatches }: SwatchGridProps) { return ( <div className="grid grid-cols-6 gap-2" role="group"> {swatches.map((swatch) => ( <button aria-label={swatch} aria-pressed={swatch.toLowerCase() === selected.toLowerCase()} className={cn( "size-7 rounded-md border outline-none ring-offset-background focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", swatch.toLowerCase() === selected.toLowerCase() && "ring-2 ring-ring", )} key={swatch} onClick={() => { onSelect(swatch); }} style={{ backgroundColor: swatch }} type="button" /> ))} </div> ); } /** Popover colour picker with swatches, a hue slider, and a hex input. */ export type ColorPickerProps = { className?: string; defaultValue?: string; onValueChange?: (value: string) => void; swatches?: string[]; value?: string; }; const ColorPicker = ({ className, defaultValue = "#3b82f6", onValueChange, ref, swatches = presetSwatches, value, }: ColorPickerProps & { ref?: React.Ref<HTMLButtonElement> }) => { const [internalValue, setInternalValue] = React.useState(defaultValue); const currentValue = value ?? internalValue; const update = (next: string) => { if (value === undefined) { setInternalValue(next); } onValueChange?.(next); }; return ( <Popover> <PopoverTrigger asChild> <Button className={cn("w-full justify-start gap-2 font-normal", className)} ref={ref} variant="outline" > <span aria-hidden className="size-4 rounded-sm border" style={{ backgroundColor: currentValue }} /> <span className="uppercase tabular-nums">{currentValue}</span> </Button> </PopoverTrigger> <PopoverContent align="start" className="w-56 space-y-3"> <SwatchGrid onSelect={update} selected={currentValue} swatches={swatches} /> <input aria-label="Hue" className="h-3 w-full cursor-pointer appearance-none rounded-full" max={360} min={0} onChange={(event) => { update(hueToHex(Number(event.target.value))); }} style={{ background: "linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%)", }} type="range" /> <Input aria-label="Hex value" onChange={(event) => { update(event.target.value); }} value={currentValue} /> </PopoverContent> </Popover> ); }; ColorPicker.displayName = "ColorPicker"; export { ColorPicker };

Stories

Explore every variant and state in the interactive Storybook:

Preview

Switch between light and dark to inspect the embedded Storybook preview.

Dependencies

  • @vllnt/ui@^0.3.0