Skip to content
Components

ColorPicker

Pick any color. A trigger shows the current color; it opens a panel with a saturation and brightness area, a hue bar and a hex field. The value is always a 6-digit hex string.

Preview

Props

  • value
  • defaultValue
  • open
  • defaultOpen
  • variant
  • triggerLabel
  • size
  • swatches
  • showHexField
  • align
  • areaLabel
  • hueLabel
  • saturationLabel
  • brightnessLabel
  • hexLabel
  • hexPlaceholder
  • hexError
  • title
  • disabled
  • invalid
import { ColorPicker } from "@/components/ui/color-picker"

<ColorPicker aria-label="Brand color" defaultValue="#7c5cff" />

Usage

When to use

  • Any color is a valid answer: a brand or accent color, a label color, a chart series color.
  • A color the user wants to see change the page while they drag.

When not to use

  • A few named colors. Use RadioGroup with swatches.
  • A color with transparency. It picks opaque colors only.
  • A hex value typed from a design file with nothing else to adjust. Use Input.

Variants

variant

input shows swatch and hex like a field, for forms. swatch is a square swatch-only button, for toolbars and headers.

  • inputdefault

  • swatch

import { ColorPicker } from "@/components/ui/color-picker"

<ColorPicker aria-label="Brand color" defaultValue="#7c5cff" />
<ColorPicker aria-label="Brand color" defaultValue="#7c5cff" variant="swatch" />

size

Trigger height from size-element-*, same as Input. The panel does not change with size.

  • sm

  • mddefault

  • lg

  • xl

import { ColorPicker } from "@/components/ui/color-picker"

<ColorPicker aria-label="Brand color" defaultValue="#7c5cff" size="sm" />
<ColorPicker aria-label="Brand color" defaultValue="#7c5cff" />
<ColorPicker aria-label="Brand color" defaultValue="#7c5cff" size="lg" />
<ColorPicker aria-label="Brand color" defaultValue="#7c5cff" size="xl" />

align

Panel alignment against the trigger, same as Popover. Ignored below md, where the panel is a bottom Sheet.

  • startdefault

  • center

  • end

import { ColorPicker } from "@/components/ui/color-picker"

<ColorPicker aria-label="Brand color" defaultValue="#7c5cff" />
<ColorPicker aria-label="Brand color" defaultValue="#7c5cff" align="center" />
<ColorPicker aria-label="Brand color" defaultValue="#7c5cff" align="end" />

Examples

Basic

import { ColorPicker } from "@/components/ui/color-picker"

<ColorPicker aria-label="Brand color" defaultValue="#7c5cff" />

Named trigger

import { ColorPicker } from "@/components/ui/color-picker"

<ColorPicker aria-label="Accent color" triggerLabel="Accent" />

Live preview, save on release

import { ColorPicker } from "@/components/ui/color-picker"

<ColorPicker aria-label="Accent color" defaultValue="#7c5cff" onValueChange={previewColor} onValueCommit={saveColor} />

Swatch trigger with presets

import { ColorPicker } from "@/components/ui/color-picker"

<ColorPicker aria-label="Label color" variant="swatch" size="sm" swatches={["#d93a3a", "#e0a020", "#2a9d5c", "#2f7de1", "#7c5cff"]} defaultValue="#2a9d5c" />

Disabled

import { ColorPicker } from "@/components/ui/color-picker"

<ColorPicker aria-label="Brand color" defaultValue="#7c5cff" disabled />

Do and don't

Do

  • Use it when any color is a valid answer: brand or accent color, label color, chart series color.
  • Save in onValueCommit. Use onValueChange only for live preview.
  • Give it an aria-label that says what the color is for.
  • Use variant swatch in tight spaces like a header or toolbar, variant input in forms.
  • Keep contrast fixes, history like recently used colors, and saving to storage in the app. The component only picks a hex.
  • Pass translated aria-label, areaLabel, hueLabel, saturationLabel, brightnessLabel and hexLabel on non-English pages.

Don't

  • Don't use it to choose between a few named colors. Use RadioGroup or SegmentedControl with color swatches.
  • Don't add a color library for it. Hex, RGB and HSV conversion is a few lines inside the component.
  • Don't use the native color input inside it; it looks different on every platform.
  • Don't change the theme on every onValueChange without throttling to one update per frame.
  • Don't put it in a Dialog footer or a Menu; the panel needs its own space.

Props

NameTypeValuesDefaultDescription
valuestring--Controlled color, lowercase 6-digit hex like "#7c5cff". Pair with onValueChange.
defaultValuestring-#111111Starting color when uncontrolled.
onValueChangefunction--(value: string) => void. Fires on every change while dragging, on each key step and on a valid hex commit. Can fire many times per second; the caller decides whether to throttle.
onValueCommitfunction--(value: string) => void. Fires once when the pointer is released, after a key step, on a valid hex commit, and when the panel closes with a changed color. Save or send the value here, not in onValueChange.
openboolean--Controlled open state of the panel. Pair with onOpenChange.
defaultOpenboolean-falseStarting open state when uncontrolled.
onOpenChangefunction--(open: boolean) => void.
variantenuminput | swatchinputinput shows swatch and hex like a field, for forms. swatch is a square swatch-only button, for toolbars and headers.
triggerLabelstring--Visible text in place of the hex on an input trigger, such as Accent. The swatch stays visible and the accessible name still includes the current hex. Ignored on a swatch trigger.
sizeenumsm | md | lg | xlmdTrigger height from size-element-*, same as Input. The panel does not change with size.
swatchesstring--Optional string[] of hex values shown as presets under the hex field, e.g. ["#d93a3a", "#2a9d5c"]. Leave empty to hide the row. (The spec schema has no array type, so it is declared as string.)
showHexFieldboolean-trueShow the hex field. It is the main way for screen reader users to pick a color, so turn it off only when another text entry for the same value sits right next to the picker.
alignenumstart | center | endstartPanel alignment against the trigger, same as Popover. Ignored below md, where the panel is a bottom Sheet.
areaLabelstring-Saturation and brightnessAccessible name of the area thumb. Pass a translated string in other languages.
hueLabelstring-HueAccessible name of the hue thumb.
saturationLabelstring-saturationAxis word read in the area's aria-valuetext after the hex, e.g. "#7c5cff, saturation 64%".
brightnessLabelstring-brightnessAxis word read in the area's aria-valuetext when brightness changed, e.g. "#7c5cff, brightness 40%".
hexLabelstring-HexVisible label of the hex field.
hexPlaceholderstring--Placeholder of the hex field.
hexErrorstring--Shown under the hex field, with role="alert", while the typed text is not a color. Pass it translated.
titlestring--A visible heading at the top of the panel. It also names the Sheet below md; without it the Sheet is named by aria-label, visually hidden.
childrennode--App-specific content under the presets, such as a reset link, a help line or export buttons. The component itself holds no copy beyond its label props.
disabledboolean-falseTrigger can't be focused or opened.
invalidboolean-falseMarks the trigger invalid in a form (aria-invalid). The hex field also shows invalid on its own while the typed text is not a color.
namestring--Form field name. Renders a hidden input with the committed hex.

Anatomy

PartDescription
rootColorPicker. Holds the value and the open state; renders nothing itself. Built on Popover.
triggerColorPickerTrigger: the button that opens the panel. variant input looks like Input (same fill, border, sizes) with the swatch at the start and the hex in font-family-mono after it. variant swatch is a square button of size-element-* with only the swatch inside, for toolbars and headers. triggerLabel replaces the visible hex with a UI label in font-family-sans, keeping the hex in its accessible name.
swatchA square of the current color, 16px in sm, md and lg, 20px in xl, radius-inner, 1px color-border-strong so white and near-page colors still show their edge. Its fill is the value itself, not a token. Disabled, it dims to opacity-disabled, since no token can grey out the value.
contentColorPickerContent: from md up, PopoverContent with the same surface, border, radius, shadow and padding as Popover, 288px wide (never wider than the screen minus spacing-sm each side). Below md, a Sheet with side="bottom" holding the same children, full width minus the Sheet padding. Children stack with gap spacing-md. A disabled picker opened from code dims the whole panel to opacity-disabled (the panel carries data-disabled itself, since it is portaled).
areaColorPickerArea: the saturation (x) and brightness (y) field for the current hue. Full content width, aspect ratio 8:5 in both Popover and Sheet (256 by 160 at 288px), radius-inner, touch-action none so a drag never scrolls the page or swipes the Sheet closed. Its gradient is computed from the hue, not a token.
areaThumb16px circle that marks the current saturation and brightness: solid color-background-raised fill, 1px color-border-strong edge, shadow-low. Raised fill and strong border are a light and dark pair in both modes, so the thumb shows on any color. The point is its center; it may overhang the area edge by half its width.
hueColorPickerHue: horizontal bar of all hues, full content width, 12px tall, radius-full, touch-action none. Its gradient is computed, not a token.
hueThumbSame 16px thumb as areaThumb, vertically centered on the bar.
hexFieldColorPickerInput: Input, font-family-mono, the swatch as its leading slot, maxLength 7. Size sm in the Popover, lg in the Sheet. Commits on Enter and on blur; Escape reverts to the last valid value.
swatchesColorPickerSwatches: optional row of preset colors under the hex field, gap spacing-xs, wraps. Each preset is a radius-inner button with the same border as swatch: 20px in the Popover, size-element-lg in the Sheet. The one equal to the value is selected.
hitAreaTransparent ::before centered on the trigger, each thumb and each preset, never smaller than size-target-min.

Accessibility

Role: button

KeysAction
Enter / SpaceOn the trigger: open the panel. Focus moves to the area thumb.
EscapeClose the panel (Popover or Sheet). Focus returns to the trigger. Commits the color if it changed.
Tab / Shift+TabMove between area thumb, hue thumb, hex field and swatches inside the panel.
ArrowLeft / ArrowRightOn the area: saturation down or up 1%. On the hue bar: hue down or up 1 degree.
ArrowDown / ArrowUpOn the area: brightness down or up 1%. On the hue bar: same as Left and Right.
Shift+ArrowStep 10% on the area, 10 degrees on the hue bar.
PageUp / PageDownSame as Shift+Arrow.
Home / EndOn the area: saturation 0% or 100%. On the hue bar: 0 or 359 degrees.
EnterIn the hex field: commit the typed value if valid. If not valid, keep the field invalid and the color unchanged.
  • aria-label (or aria-labelledby) is required: it says what the color is for, e.g. "Accent color". The trigger's accessible name is that label plus the current hex, e.g. "Accent color, #7c5cff".
  • The area thumb and the hue thumb are each role="slider", named by areaLabel and hueLabel. A slider has only one aria-valuenow: on the area it follows the axis that last changed (saturation for Left/Right, brightness for Up/Down), so every key press is read back. aria-valuetext is the hex plus that axis, built from the label props, e.g. "#7c5cff, saturation 64%" or, on a Vietnamese page, "#7c5cff, độ bão hòa 64%". On the hue bar it is the hex plus degrees, e.g. "#7c5cff, 252°".
  • Screen readers on phones only offer increase and decrease, so they reach one axis of the area. The hex field is the main way for screen reader users to pick a color; the area and hue bar are for pointer and keyboard. Never hide the hex field when there is no other text entry nearby.
  • The hex field is an Input labeled by hexLabel. It accepts 3 or 6 digits, with or without #, any case, and writes back lowercase 6-digit hex on commit.
  • Every thumb and swatch keeps a ::before hit area of at least size-target-min (24px mouse, 44px touch).
  • No alpha channel in v1. No color names are announced: the component ships no color library, so hex is the only spoken value.
  • Hue keeps its own state while saturation or brightness is 0, so moving back up from black or gray does not jump the hue to red.
  • Below md the panel opens as a Sheet from the bottom. The Sheet title is the picker's aria-label (visually hidden is fine). Closing it by swipe, scrim or Escape commits like Escape and returns focus to the trigger.
  • Every built-in string (areaLabel, hueLabel, saturationLabel, brightnessLabel, hexLabel) is a prop so apps can translate it. The component holds no copy of its own beyond those English defaults.

Install

npx shadcn add https://werewolf-ui.vercel.app/r/color-picker.json

This also adds styles/werewolf.css, the tokens every component uses, and any other component it needs. Import that file once in your root CSS, after @import "tailwindcss", with a path relative to that CSS file (from app/globals.css it is @import "../styles/werewolf.css").