# 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.

## 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.

Values: `input`, `swatch`. Default: `input`.

### size

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

Values: `sm`, `md`, `lg`, `xl`. Default: `md`.

### align

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

Values: `start`, `center`, `end`. Default: `start`.

## Examples

### Basic

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

### Named trigger

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

### Live preview, save on release

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

### Swatch trigger with presets

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

### Disabled

```tsx
<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

| Name | Type | Values | Default | Description |
| --- | --- | --- | --- | --- |
| `value` | `string` |  |  | Controlled color, lowercase 6-digit hex like "#7c5cff". Pair with onValueChange. |
| `defaultValue` | `string` |  | `#111111` | Starting color when uncontrolled. |
| `onValueChange` | `function` |  |  | (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. |
| `onValueCommit` | `function` |  |  | (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. |
| `open` | `boolean` |  |  | Controlled open state of the panel. Pair with onOpenChange. |
| `defaultOpen` | `boolean` |  | `false` | Starting open state when uncontrolled. |
| `onOpenChange` | `function` |  |  | (open: boolean) => void. |
| `variant` | `enum` | `input`, `swatch` | `input` | input shows swatch and hex like a field, for forms. swatch is a square swatch-only button, for toolbars and headers. |
| `triggerLabel` | `string` |  |  | 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. |
| `size` | `enum` | `sm`, `md`, `lg`, `xl` | `md` | Trigger height from size-element-*, same as Input. The panel does not change with size. |
| `swatches` | `string` |  |  | 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.) |
| `showHexField` | `boolean` |  | `true` | Show 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. |
| `align` | `enum` | `start`, `center`, `end` | `start` | Panel alignment against the trigger, same as Popover. Ignored below md, where the panel is a bottom Sheet. |
| `areaLabel` | `string` |  | `Saturation and brightness` | Accessible name of the area thumb. Pass a translated string in other languages. |
| `hueLabel` | `string` |  | `Hue` | Accessible name of the hue thumb. |
| `saturationLabel` | `string` |  | `saturation` | Axis word read in the area's aria-valuetext after the hex, e.g. "#7c5cff, saturation 64%". |
| `brightnessLabel` | `string` |  | `brightness` | Axis word read in the area's aria-valuetext when brightness changed, e.g. "#7c5cff, brightness 40%". |
| `hexLabel` | `string` |  | `Hex` | Visible label of the hex field. |
| `hexPlaceholder` | `string` |  |  | Placeholder of the hex field. |
| `hexError` | `string` |  |  | Shown under the hex field, with role="alert", while the typed text is not a color. Pass it translated. |
| `title` | `string` |  |  | 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. |
| `children` | `node` |  |  | 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. |
| `disabled` | `boolean` |  | `false` | Trigger can't be focused or opened. |
| `invalid` | `boolean` |  | `false` | Marks 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. |
| `name` | `string` |  |  | Form field name. Renders a hidden input with the committed hex. |

## Anatomy

| Part | Description |
| --- | --- |
| `root` | ColorPicker. Holds the value and the open state; renders nothing itself. Built on Popover. |
| `trigger` | ColorPickerTrigger: 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. |
| `swatch` | A 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. |
| `content` | ColorPickerContent: 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). |
| `area` | ColorPickerArea: 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. |
| `areaThumb` | 16px 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. |
| `hue` | ColorPickerHue: horizontal bar of all hues, full content width, 12px tall, radius-full, touch-action none. Its gradient is computed, not a token. |
| `hueThumb` | Same 16px thumb as areaThumb, vertically centered on the bar. |
| `hexField` | ColorPickerInput: 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. |
| `swatches` | ColorPickerSwatches: 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. |
| `hitArea` | Transparent ::before centered on the trigger, each thumb and each preset, never smaller than size-target-min. |

## Accessibility

Role: `button`

| Keys | Action |
| --- | --- |
| `Enter / Space` | On the trigger: open the panel. Focus moves to the area thumb. |
| `Escape` | Close the panel (Popover or Sheet). Focus returns to the trigger. Commits the color if it changed. |
| `Tab / Shift+Tab` | Move between area thumb, hue thumb, hex field and swatches inside the panel. |
| `ArrowLeft / ArrowRight` | On the area: saturation down or up 1%. On the hue bar: hue down or up 1 degree. |
| `ArrowDown / ArrowUp` | On the area: brightness down or up 1%. On the hue bar: same as Left and Right. |
| `Shift+Arrow` | Step 10% on the area, 10 degrees on the hue bar. |
| `PageUp / PageDown` | Same as Shift+Arrow. |
| `Home / End` | On the area: saturation 0% or 100%. On the hue bar: 0 or 359 degrees. |
| `Enter` | In 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

```bash
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"`).
