Color Picker
A swatch-triggered popover that presents a grid of preset colors — pick one, fire onValueChange, mark the selection with a check.
- Status
- Since
0.1.0- Accessibility pattern
- APG dialog (non-modal) picker
Last updated
Install
Add Color Picker from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.
pnpm dlx shadcn@latest add @vegastack/color-pickerThe same command installs the registry items it composes: @vegastack/icon-button, @vegastack/popover, @vegastack/use-list-nav.
Usage
import { useState } from "react";
import { ColorPicker } from "@/components/ui/color-picker";
function Example() {
const [color, setColor] = useState("blue");
return <ColorPicker value={color} onValueChange={setColor} />;
}ColorPicker is controlled — pass value (matched against each option's name) and onValueChange (called with the picked color's name). The trigger is a rounded-md outline control with a rounded-sm fill chip showing the current selection; opening it reveals the palette. Only the in-grid preset swatches are rounded-full.
Examples
Palette
The colors are data backed by semantic tokens. The default palette exposes stable hue names
(green, blue, rose, …), and each swatch uses VegaStack semantic CSS variables — info,
status, neutral, and chart tokens — rather than raw Tailwind palette variables. Pass your own
colors to override:
import { ColorPicker, type ColorOption } from "@/components/ui/color-picker";
const palette: ColorOption[] = [
{ name: "info", label: "Info", color: "var(--color-info)" },
{ name: "accent", label: "Accent", color: "var(--color-accent)" },
{ name: "danger", label: "Danger", color: "var(--color-destructive)" },
];
<ColorPicker
value="blue"
onValueChange={setColor}
colors={palette}
columns={3}
/>;Each ColorOption is { name, label, color }:
name— the stable value the picker emits and matches selection against.label— the swatch's accessible name (aria-label) and tooltip.color— a CSS color value. Prefer semantic variables likevar(--color-info)orvar(--color-chart-2). Arbitrary CSS colors are reserved for dynamic user-authored palette data; this is the one place the design system renders a dynamic swatch fill via inlinestyle={{ backgroundColor }}. Every other style (sizing, borders, focus, spacing) uses semantic tokens.
DEFAULT_COLORS
When you omit colors, the picker renders the exported DEFAULT_COLORS palette — 12 named options,
each mapped to a VegaStack semantic token (status, neutral, or chart series) so the registry
component never ships raw Tailwind palette variables:
name | label | color token |
|---|---|---|
gray | Gray | var(--color-primary) |
red | Red | var(--color-destructive) |
orange | Orange | var(--color-chart-4) |
amber | Amber | var(--color-warning) |
yellow | Yellow | var(--color-chart-7) |
green | Green | var(--color-success) |
teal | Teal | var(--color-chart-2) |
sky | Sky | var(--color-info) |
blue | Blue | var(--color-chart-8) |
indigo | Indigo | var(--color-chart-3) |
pink | Pink | var(--color-chart-5) |
rose | Rose | var(--color-chart-6) |
Controlled selection
States
Default (a selection chosen), empty (no selection yet — a transparent trigger), and disabled.
Custom palette
Pass colors to render your own curated set — here a 3-color tag palette in a 3-wide grid.
Columns
columns sets the swatch-grid width (default 7), driving the dynamic --swatch-cols grid
template. Below: the default 12-color palette at columns={4}, 7, and 14.
API Reference
ColorPicker
| Prop | Type | Default | Description |
|---|---|---|---|
aria-label | string | "Pick a color" | Accessible name for the trigger button. |
className | string | — | Extra classes for the trigger swatch button. |
colors | readonly ColorOption[] | DEFAULT_COLORS | The palette to render. |
columns | number | 7 | Number of columns in the swatch grid. |
disabled | boolean | false | Disables the trigger and every swatch. |
onValueChange | ((value: string) => void) | — | Fired when a swatch is picked, with the chosen ColorOption.name. |
ref | React.Ref<HTMLButtonElement> | — | Ref forwarded to the trigger button — the component's focusable root (the popover content is portaled, so the trigger is the stable host element to focus/measure). |
value | string | — | The currently selected color, matched against each ColorOption.name. When it matches an option,
that swatch shows a check and the trigger renders its color. |
Data attributes and CSS variables on ColorPicker
| Attribute | Values |
|---|---|
data-slot | "color-picker" | "color-picker-check" | "color-picker-swatch" |
--swatch-cols | CSS custom property |
ColorOption
The shape of each entry in a colors palette.
ColorOption
| Prop | Type | Default | Description |
|---|---|---|---|
color* | string | — | Any CSS color the swatch renders as its background. Prefer semantic design-token variables
(var(--color-info), var(--color-chart-2), …). Consumer-provided arbitrary colors are
allowed only as dynamic user data, so the value is applied via inline style (the sanctioned
exception — see the file header). |
label* | string | — | Human-readable label, used as the swatch's accessible name (aria-label) and its tooltip
(title) — e.g. "Blue". |
name* | string | — | Stable identifier for the color — this is the value onValueChange emits and the value matched
against value to determine the selected swatch (e.g. "blue"). |
Accessibility
- The trigger is a
Buttonwith an accessible name (aria-label, default"Pick a color"); customize it to describe what the color is for (e.g."Label color"). - Each preset is a
<button>labelled by its color name, grouped in arole="group"labelled"Colors". The selected swatch carriesaria-pressed="true"and uses a semantic-surface check badge (bg-background text-foreground) so the check stays legible on light or custom swatches. - The picker opens on trigger click; focus moves into the grid and is restored to the trigger on close.
- Dismiss is built in — clicking outside the panel or pressing Esc closes it.
- Every control keeps the global 2px
:focus-visiblering (outline-ring); theoutline-variant trigger additionally re-colors its border with theringtoken (focus-visible:border-ring/(--alpha-tint-border)), and theghostswatch buttons rely on that global ring — neveroutline: none.
| Key | Action |
|---|---|
| Enter / Space | Open the picker (on the trigger), or select the focused swatch. |
| Tab / Shift + Tab | Move focus between swatches in the open grid. |
| Esc | Close the picker and return focus to the trigger. |
| Contract | States tested |
|---|---|
| Behaviour | default, active, disabled, error, pressed, range, selected, success |
| Accessibility | disabled, keyboard, labeled, pressed |
| Visual | default, hover, error, success |