Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
VegaStack Design

Color Picker

A swatch-triggered popover that presents a grid of preset colors — pick one, fire onValueChange, mark the selection with a check.

Status
stable
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-picker

The 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 like var(--color-info) or var(--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 inline style={{ 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:

namelabelcolor token
grayGrayvar(--color-primary)
redRedvar(--color-destructive)
orangeOrangevar(--color-chart-4)
amberAmbervar(--color-warning)
yellowYellowvar(--color-chart-7)
greenGreenvar(--color-success)
tealTealvar(--color-chart-2)
skySkyvar(--color-info)
blueBluevar(--color-chart-8)
indigoIndigovar(--color-chart-3)
pinkPinkvar(--color-chart-5)
roseRosevar(--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

PropTypeDefaultDescription
aria-labelstring"Pick a color"Accessible name for the trigger button.
classNamestringExtra classes for the trigger swatch button.
colorsreadonly ColorOption[]DEFAULT_COLORSThe palette to render.
columnsnumber7Number of columns in the swatch grid.
disabledbooleanfalseDisables the trigger and every swatch.
onValueChange((value: string) => void)Fired when a swatch is picked, with the chosen ColorOption.name.
refReact.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).
valuestringThe 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

AttributeValues
data-slot"color-picker" | "color-picker-check" | "color-picker-swatch"
--swatch-colsCSS custom property

ColorOption

The shape of each entry in a colors palette.

ColorOption

PropTypeDefaultDescription
color*stringAny 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*stringHuman-readable label, used as the swatch's accessible name (aria-label) and its tooltip (title) — e.g. "Blue".
name*stringStable 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 Button with 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 a role="group" labelled "Colors". The selected swatch carries aria-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-visible ring (outline-ring); the outline-variant trigger additionally re-colors its border with the ring token (focus-visible:border-ring/(--alpha-tint-border)), and the ghost swatch buttons rely on that global ring — never outline: none.
KeyAction
Enter / SpaceOpen the picker (on the trigger), or select the focused swatch.
Tab / Shift + TabMove focus between swatches in the open grid.
EscClose the picker and return focus to the trigger.
ContractStates tested
Behaviourdefault, active, disabled, error, pressed, range, selected, success
Accessibilitydisabled, keyboard, labeled, pressed
Visualdefault, hover, error, success

Do / Don't

Do
Use a ColorPicker for a small, curated set of preset colors — labels, tags, calendar categories. Give the trigger a descriptive aria-label, and emit a stable name you can persist.
Don't
Use it as a full-spectrum color picker (hue/saturation/alpha) — that's a different control. And don't hardcode hex in your palette when a semantic token (var(--color-…)) is available.

On this page