Skip to content
Component installs need the registry setup
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

Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Color Picker before installation. Retain the digest printed by this command.

pnpm exec vegastack-design verify color-picker

Install with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.

pnpm dlx shadcn@4.21.0 add @vegastack/color-picker

Run the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.

The same command installs the registry items it composes: @vegastack/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)

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.

Hue palette, inline

HUE_COLORS is the ten tag hues — the exact colours Chip, Avatar and IconGlyph draw — so the stored value ("blue", "purple"…) is a hue those components take. variant="inline" renders the swatches in the page with no trigger, and onClear adds a separate clear action, selected while value matches no hue. Pick columns so the row fits its container (six wraps the eleven swatches onto two rows at a phone's width).

Columns

columns sets the swatch-grid width (default 6), 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.
classNamestring—Extra classes for the trigger swatch button.
clearLabelstring"None"The clear action's accessible name.
closeOnSelectbooleanfalseClose after picking a swatch.
colorsreadonly ColorOption[]DEFAULT_COLORSThe palette to render.
columnsnumber6Number of columns in the swatch grid.
disabledbooleanfalseDisables the trigger and every swatch.
onClear(() => void)—Adds a separate clear action, selected while value matches no option.
onOpenChange((open: boolean) => void)—Popup state callback.
onValueChange((value: string) => void)—Fired when a swatch is picked, with the chosen ColorOption.name.
openboolean—Controlled popup state.
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).
triggerReact.ReactElement<unknown, string | React.JSXElementConstructor<any>>—Custom trigger composed through Base UI.
valuestring—The currently selected color, matched against each ColorOption.name. When it matches an option, that swatch marks its outer cell and the trigger renders its color.
variant"inline" | "popover""popover"popover — a swatch trigger that opens the grid. inline — the swatch grid itself, in the page (a settings row), with no trigger.

Data attributes and CSS variables on ColorPicker

AttributeValues
data-slot"color-picker" | "color-picker-content" | "color-picker-swatch"
data-variantmirrors a prop or state value
--swatch-colsCSS custom property

ColorOption

The shape of each entry in a colors palette.

PropTypeDefaultDescription
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 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 persistent outer-cell border, leaving the colour fill unobscured.
  • 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 keyboard-focus background tint from base.css; no component adds a focus ring or changes its border on focus.
KeyAction
Enter / SpaceOpen the picker (on the trigger), or select the focused swatch.
← / →Move one swatch within the grid.
↑ / ↓Move one row — by columns swatches.
Home / EndJump to the first or last swatch in the grid.
Tab / Shift + TabLeave the grid — roving tabindex makes it one tab stop.
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