# Color picker

Lets people choose a colour on a saturation and brightness area, a hue slider and a hex field.

- **Import:** `import { ColorPicker, ColorPickerPopover, ColorSwatch } from "@booleanpress/ui/color-picker"`
- **APG Slider:** <https://www.w3.org/WAI/ARIA/apg/patterns/slider/>
- **Page:** <https://ui.booleanpress.com/components/color-picker> · @booleanpress/ui 0.2.0

## Usage

`ColorPicker` draws the picker in the page; `ColorPickerPopover` draws a swatch button that opens it in a popover. Both take the same props.

```tsx
import { ColorPicker, ColorPickerPopover } from "@booleanpress/ui/color-picker"

export function ButtonColour() {
  return (
    <>
      <ColorPicker defaultValue="#276def" aria-label="Button colour" />
      <ColorPickerPopover defaultValue="#276def" aria-label="Link colour" />
    </>
  )
}
```

The value is a hex string (`#rgb`, `#rrggbb`, with or without alpha); a value that is not hex is ignored and the picker shows black or the colour it had. It is uncontrolled with `defaultValue` (`#000000` by default), or controlled with `value` and `onValueChange`, which fires while the colour moves; `onValueCommit` fires once, when a drag, a key press or an edit of the hex field ends, which suits saving. `alpha` adds an alpha slider; the value then reads `#rrggbbaa` while the colour is not opaque. `swatches` adds preset colours under the picker: hex strings, or `{ value, label }` to give each a spoken name. `orientation="vertical"` stands the hue (and alpha) slider beside the area. `size` and `variant` set the hex field's size and look and follow the provider; `name` submits the value in a form, as lower-case hex; a reset of the form brings back `defaultValue`, and a disabled picker is not submitted. Name the picker with `aria-label` or `aria-labelledby`; it is "Colour picker" by default, the provider string `colorPicker`. `ColorSwatch` draws a colour square on its own, for a list of saved colours.

## Examples

### Basic

The area, the hue slider, a preview and the hex field, in the page.

```tsx
import { ColorPicker } from "@booleanpress/ui/color-picker"

export default function ColorPickerBasic() {
  return <ColorPicker defaultValue="#276def" aria-label="Button colour" />
}
```

### In a popover

`ColorPickerPopover` opens the picker from a 36 px swatch button named with the label and the colour.

```tsx
import { ColorPickerPopover } from "@booleanpress/ui/color-picker"

export default function ColorPickerInPopover() {
  return (
    <div className="flex items-center gap-3">
      <ColorPickerPopover defaultValue="#ff0000" aria-label="Accent colour" />
      <span className="text-sm/normal text-foreground">Accent colour</span>
    </div>
  )
}
```

### Vertical hue slider

`orientation="vertical"` stands the hue and alpha sliders beside the area, the highest value at the top.

```tsx
import { ColorPicker } from "@booleanpress/ui/color-picker"

export default function ColorPickerVertical() {
  return <ColorPicker orientation="vertical" alpha defaultValue="#ff0000" aria-label="Header colour" className="max-w-sm" />
}
```

### Controlled

`value` and `onValueChange` show the hex while it moves; `onValueCommit` reports it once each change ends.

```tsx
import { useState } from "react"
import { ColorPicker } from "@booleanpress/ui/color-picker"

export default function ColorPickerControlled() {
  const [colour, setColour] = useState("#0f766e")
  const [saved, setSaved] = useState("#0f766e")

  return (
    <div className="flex w-full max-w-xs flex-col gap-3">
      <p className="text-center font-mono text-sm/normal text-muted-foreground">
        onValueChange: {colour}
        <br />
        onValueCommit: {saved}
      </p>
      <ColorPicker value={colour} onValueChange={setColour} onValueCommit={setSaved} aria-label="Email header colour" />
    </div>
  )
}
```

### Swatches

`swatches` adds preset brand colours, each named by its label; the one matching the colour is chosen.

```tsx
import { ColorPicker } from "@booleanpress/ui/color-picker"

const BRAND = [
  { value: "#020617", label: "Ink" },
  { value: "#2563eb", label: "Mailer blue" },
  { value: "#0f766e", label: "Teal" },
  { value: "#16a34a", label: "Delivered green" },
  { value: "#ca8a04", label: "Queued amber" },
  { value: "#dc2626", label: "Bounce red" },
  { value: "#9333ea", label: "Violet" },
]

export default function ColorPickerSwatches() {
  return <ColorPicker defaultValue="#2563eb" swatches={BRAND} aria-label="Brand colour" />
}
```

### With alpha

`alpha` adds an alpha slider over checks; the value carries an alpha pair.

```tsx
import { ColorPicker } from "@booleanpress/ui/color-picker"

export default function ColorPickerAlpha() {
  return <ColorPicker alpha defaultValue="#2563eb99" aria-label="Overlay colour" />
}
```

### Disabled

`disabled` dims the picker and takes its sliders and field out of the tab order.

```tsx
import { ColorPicker } from "@booleanpress/ui/color-picker"

export default function ColorPickerDisabled() {
  return <ColorPicker disabled defaultValue="#64748b" aria-label="Locked theme colour" />
}
```

## Accessibility

**Semantics.** The picker is a `role="group"`. The area follows React Aria's ColorArea: its thumb holds two visually hidden range inputs, saturation (across) and brightness (up), so each is a `slider` with its own name and value text. The hue and alpha sliders are range inputs too, so touch screen readers can swipe them. The hex field is a text input; the swatches are a radio group.

**Labels.** Name the picker with `aria-label` or `aria-labelledby` ("Colour picker" by default). The sliders are named "Saturation", "Brightness", "Hue" and "Alpha", the field "Hex colour" and the swatches "Preset colours" (the provider strings `saturation`, `brightness`, `hue`, `alpha`, `hexColor` and `colorSwatches`); values are read as percentages and degrees in the provider's locale. A swatch reads its `label`, else its hex.

**Focus.** Tab moves through the area (one stop: the input used last, saturation at first), the hue slider, the alpha slider, the hex field and the swatches (one stop). A thumb shows the 1px `--ring` outline 2px outside while it has keyboard focus. In a popover, focus moves into the picker on open and back to the swatch button on close.

**Known limits.**

- Colour is chosen by sight. The hex field and the swatches' names are the ways to choose an exact colour without it.
- There is no eyedropper and no other format than hex (RGB, HSL); convert the hex value yourself.
- In right-to-left pages the area and the horizontal sliders run from right to left, and the arrow keys follow them.

### Keyboard

| Key | Behaviour |
| --- | --- |
| ← or → | On the area, lowers or raises the saturation by 1 and moves focus to the saturation input; on a slider, lowers or raises its value. Swapped in right-to-left pages, where the area and the sliders run from right to left. |
| ↑ or ↓ | On the area, raises or lowers the brightness by 1 and moves focus to the brightness input; on a slider, raises or lowers its value. |
| Shift + → | With any arrow key, moves 10 that way. |
| Page Up or Page Down | On the area, raises or lowers the brightness by 10; on a slider, its value by 10. |
| Home or End | On the area, lowers or raises the saturation by 10 (as React Aria's ColorArea); on a slider, sets its minimum or maximum. |
| Enter | In the hex field, applies the colour typed and shows its hex again. |
| ← or → or ↑ or ↓ | In the swatches, chooses the previous or next preset colour. |

## API

### ColorPicker

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `alpha` | `boolean` | `false` | Adds an alpha slider; the value then carries an alpha pair (`#rrggbbaa`) when the colour is not opaque. |
| `defaultValue` | `string` | `#000000` | The colour at the start, when it controls itself. `#000000` by default. |
| `disabled` | `boolean` | `false` | Dims the picker and ignores the pointer and the keyboard. |
| `form` | `string` |  | The id of the form the value belongs to, for a control placed outside it. |
| `name` | `string` |  | The form field name; the hex value is submitted with the form. |
| `onValueChange` | `((value: string) => void)` |  | Called with the new hex colour while it changes. |
| `onValueCommit` | `((value: string) => void)` |  | Called with the hex colour once a drag, a key press or an edit of the hex field ends. |
| `orientation` | `"horizontal" \| "vertical"` | `horizontal` | `vertical` puts the hue (and alpha) slider upright beside the area. `horizontal` by default. |
| `size` | `"default" \| "sm" \| "lg"` |  | The hex field's and swatches' size. Defaults to the provider's `controlSize`. |
| `swatches` | `readonly (string \| { value: string; label?: string; })[]` |  | Preset colours shown as swatches under the picker: hex strings, or `{ value, label }` to name them. |
| `value` | `string` |  | The colour as hex (`#3b82f6`, or `#3b82f680` with alpha), when you control it. |
| `variant` | `"default" \| "filled"` |  | The hex field's look. Defaults to the provider's `fieldVariant`. |

### ColorPickerPopover

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `align` | `"center" \| "start" \| "end"` | `start` | The popover's alignment against the swatch. `start` by default. |
| `alpha` | `boolean` | `false` | Adds an alpha slider; the value then carries an alpha pair (`#rrggbbaa`) when the colour is not opaque. |
| `defaultOpen` | `boolean` |  | Whether the popover starts open. |
| `defaultValue` | `string` | `#000000` | The colour at the start, when it controls itself. `#000000` by default. |
| `disabled` | `boolean` | `false` | Dims the picker and ignores the pointer and the keyboard. |
| `form` | `string` |  | The id of the form the value belongs to, for a control placed outside it. |
| `name` | `string` |  | The form field name; the hex value is submitted with the form. |
| `onOpenChange` | `((open: boolean) => void)` |  | Called when the popover opens or closes. |
| `onValueChange` | `((value: string) => void)` |  | Called with the new hex colour while it changes. |
| `onValueCommit` | `((value: string) => void)` |  | Called with the hex colour once a drag, a key press or an edit of the hex field ends. |
| `open` | `boolean` |  | Whether the popover is open, when you control it. |
| `orientation` | `"horizontal" \| "vertical"` |  | `vertical` puts the hue (and alpha) slider upright beside the area. `horizontal` by default. |
| `side` | `"top" \| "bottom" \| "left" \| "right"` |  | The side of the swatch the popover opens on. Below by default. |
| `size` | `"default" \| "sm" \| "lg"` |  | The hex field's and swatches' size. Defaults to the provider's `controlSize`. |
| `swatches` | `readonly (string \| { value: string; label?: string; })[]` |  | Preset colours shown as swatches under the picker: hex strings, or `{ value, label }` to name them. |
| `value` | `string` |  | The colour as hex (`#3b82f6`, or `#3b82f680` with alpha), when you control it. |
| `variant` | `"default" \| "filled"` |  | The hex field's look. Defaults to the provider's `fieldVariant`. |

### ColorSwatch

Renders a `span` and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `color` (required) | `string` |  | Any CSS colour: `#3b82f6`, `#3b82f680`, `rgb(…)`. |

Every part takes `className`, merged with its defaults by `cn()`, and `ref`, which reaches the element it renders.

**Data attributes:** `data-slot="color-picker-preview"` (ColorPicker), `data-slot="color-picker-trigger"` (ColorPickerPopover), `data-slot="color-swatch"` (ColorSwatch), and `data-orientation`, `data-size`, `data-disabled`, `data-channel`.

**Provider strings:** `hue`, `alpha`, `saturation`, `brightness`, `hexColor`, `colorPicker`, `colorSwatches` (`BooleanUIProvider`'s `strings`).

## Theming

The area, the sliders and the swatches carry the colour itself; their edge is a 1px `--border` ring inside and the checks behind transparency are `--muted` on `--background`. The thumbs are 16 px circles with a white 3px ring in both themes, so they show on any colour. A chosen swatch has a 2px `--ring` ring 2px away. The hex field is an [input](/components/input).

| Token | Used for |
| --- | --- |
| `--ring` | outline, focus ring |
