# ColorPicker

**When to use:** Hex colour selection — theme editors, annotation tools, customisation panels.

**Keywords:** color swatch, palette, hex picker

**Import:** `import { ColorPicker } from '@ahrowe/ui'`

```tsx
import { ColorPicker } from '@ahrowe/ui';

// Popover trigger (default)
<ColorPicker
  value={color}
  onChange={(hex) => setColor(hex)}
/>

// With text input field shown
<ColorPicker
  value={color}
  onChange={(hex) => setColor(hex)}
  showInput
/>

// Always-visible inline picker
<ColorPicker
  value={color}
  onChange={(hex) => setColor(hex)}
  inline
/>
```

**Key props:**

| Prop | Type | Description |
|------|------|-------------|
| `value` | `string` | Current colour. Any CSS colour the browser understands — `'#ff0000'`, `'#f00'`, `'red'`, `'rgb(255 0 0)'`, `'oklch(0.8 0.11 85)'`, `'transparent'` — normalised to hex on the way in |
| `onChange` | `(hex: string) => void` | Called with new hex on change |
| `showInput` | `boolean` | Show hex text input |
| `inline` | `boolean` | Render picker inline instead of as a popover |

An eyedropper button is shown automatically next to the hex input on browsers that support the native `EyeDropper` API (Chromium-based desktop/Android browsers). It lets the user sample a colour from anywhere on screen, not just within the page. It's hidden entirely on unsupported browsers (Firefox, Safari, iOS) — no prop needed.

**Anything in, hex out.** `value` takes any CSS colour and `onChange` always emits hex (`#rrggbb`, or `#rrggbbaa` when the colour is not fully opaque), so a round trip through the picker normalises the notation. A value the browser cannot parse at all falls back to the component's default colour rather than throwing.

Parsing uses the browser's own colour engine, which means it needs a DOM with a canvas. Under jsdom every non-hex value falls back to the default instead — worth knowing when a test asserts on a colour that went in as `rgb()`.

## Sizing

The inline swatch tracks the theme's text size, since it sits beside a label: `--swatch-size` falls back to `2em`, i.e. 32px at a 16px base, and the gap, the hex overlay and the transparency checkerboard scale with it.

The picker panel deliberately does not. It is a floating control surface with its own fixed layout — a saturation area, sliders and a hex field — and it holds together at a large base font rather than needing to grow with it.

**Labels:** `pickFromScreen`, `saturationAndBrightness`, `saturationAndBrightnessValue(saturation, brightness)` — the text this component renders of its own. Pass `labels` to override any of them, on the component or app-wide through `ConfigProvider`; they merge per key. Type: `ColorPickerLabels`.

**Keyboard:** the swatch is a button, so `Enter` or `Space` opens the panel and puts the focus on the saturation and brightness square. A click leaves the focus on the swatch, since a pointer open is not a request to be taken somewhere. `Tab` walks the rest of the panel, and `Escape` closes and hands the focus back to the swatch.

| Key | Does |
|-----|------|
| `←` `→` | Saturation, a percent at a time |
| `↑` `↓` | Brightness, a percent at a time |
| `Shift` with any arrow | Ten at a time |
| `Home` `End` | Saturation to either end |
| `PageUp` `PageDown` | Brightness to either end |

The square is a two-axis control, which ARIA has no role for. It reports itself as a slider whose value is the horizontal axis, with `aria-valuetext` carrying both numbers, so a screen reader reads out "62% saturation, 40% brightness" after every move.

**Slots:** `root` `swatch` `panel` `gradient` `hueSlider` `alphaSlider` `hexInput` `eyeDropper`
