---
title: "Color Editor"
description: "Full panel for editing a solid color or a linear gradient."
links:
  doc: https://cladd.io/react/components/color-editor/
  api: https://cladd.io/react/components/color-editor/#api-reference
---

# Color Editor

`ColorEditor` is the full, inline color-editing panel — a saturation/brightness area, a hue slider, an optional alpha slider, a row of channel scrubbers, a hex input, and an optional row of swatches. Turn on `gradient` and it gains a Solid/Gradient switch plus two-stop linear-gradient editing with an angle control. It's the surface you embed in an inspector, a theme editor, or a design-tool sidebar when picking a color _is_ the task and you want it always visible.

For the popover version — a trigger swatch that opens this same panel on demand — reach for [`ColorPicker`](/react/components/color-picker/) instead. `ColorEditor` is the panel; `ColorPicker` wraps it in a trigger.

```tsx
<div className="w-56">
  <ColorEditor
    value={color}
    onChange={(c: ColorValue) => setColor(c.hex)}
    swatches={PALETTE}
  />
</div>
```

## Usage

```tsx
import { ColorEditor } from '@cladd-ui/react';

const [color, setColor] = useState('#2f6bff');

<ColorEditor value={color} onChange={(c) => setColor(c.hex)} />;
```

The panel is **full-width** — it has no intrinsic width of its own and stretches to fill its container. Wrap it in a fixed-width element (or drop it into an inspector column) to size it. `value` accepts any CSS color string or a single channel set (`{ r, g, b }`, `{ h, s, l }`, `{ h, s, b }`); `onChange` fires on every change with a fully-resolved value carrying `hex`, `rgb`, `hsl`, `hsb`, and `css` — read whichever you need straight off it. Leave `value` off and pass `defaultValue` to run it uncontrolled. See [Values](#values) below for the full breakdown of what goes in and what comes out.

Internally the panel is built from real cladd primitives — the channel scrubbers are [`NumberScrubber`](/react/components/number-scrubber/), the hex field is an [`Input`](/react/components/input/), the Solid/Gradient switch is a [`Toolbar`](/react/components/toolbar/) — so it inherits the surface and accent of its surroundings. Drop it inside a [`Surface`](/react/components/surface/) and its focus rings and active states pick up that surface's `color`.

## Values

The editor is **permissive on the way in and exhaustive on the way out**: `value` / `defaultValue` accept a handful of loose formats, but `onChange` always hands back one fully-resolved object carrying every representation. You never have to convert before passing a color in, and you never have to convert after reading one out — just store the field you care about.

### Accepted input

`value` and `defaultValue` take a `ColorInput` — either a CSS color string or a single channel set. Alpha is optional on every form; omit it for fully opaque.

| Form                | Examples                                                             | Ranges                                |
| ------------------- | -------------------------------------------------------------------- | ------------------------------------- |
| Hex string          | `'#2f6bff'`, `'#2f6bffcc'`, `'#fff'`, `'#fff8'`                      | 3, 4, 6, or 8 digits; `#` required    |
| CSS function string | `'rgb(47 107 255)'`, `'rgba(47,107,255,0.8)'`, `'hsl(220 100% 59%)'` | modern (space) or legacy (comma) form |
| Named color         | `'red'`, `'white'`, `'transparent'`                                  | basic set only — see note             |
| RGB object          | `{ r: 47, g: 107, b: 255 }`                                          | r, g, b `0–255` · `a` `0–1`           |
| HSL object          | `{ h: 220, s: 100, l: 59 }`                                          | h `0–360` · s, l `0–100` · `a` `0–1`  |
| HSB object          | `{ h: 220, s: 82, b: 100 }`                                          | h `0–360` · s, b `0–100` · `a` `0–1`  |

A few things worth knowing:

- **Alpha is always `0–1`** on channel sets, even though hex encodes it as two extra digits (`#rrggbb**aa**`). Leave `a` off and the color is opaque.
- **Out-of-range numbers are clamped**, and an **unparseable string falls back to black** (`#000000`) rather than throwing — so a typo'd value silently becomes black.
- **Named colors cover only a basic set** — `transparent`, `black`, `white`, `gray`/`grey`, `silver`, `red`, `maroon`, `orange`, `yellow`, `olive`, `lime`, `green`, `aqua`/`cyan`, `teal`, `blue`, `navy`, `fuchsia`/`magenta`, `purple`, `pink`. Anything outside it (`coral`, `rebeccapurple`, …) is **not** recognized and falls back to black. Use hex/rgb/hsl for those.
- **Input is independent of [`format`](#channel-format).** You can pass `{ h, s, l }` and still display RGB scrubbers — `format` only controls which channels the scrubber row shows, never how you supply or receive the value.

### Emitted value

`onChange` fires on every change — area drag, hue/alpha slide, channel scrub, hex commit, swatch click — with a single `ColorValue`. It always carries **every** representation regardless of what you passed in or which `format` is on screen, so read whichever field fits:

| Field | Type             | Notes                                                           |
| ----- | ---------------- | --------------------------------------------------------------- |
| `hex` | `string`         | `#rrggbb`, or `#rrggbbaa` when alpha `< 1`                      |
| `rgb` | `{ r, g, b, a }` | r, g, b `0–255` · a `0–1`                                       |
| `hsl` | `{ h, s, l, a }` | h `0–360` · s, l `0–100` · a `0–1`                              |
| `hsb` | `{ h, s, b, a }` | h `0–360` · s, b `0–100` · a `0–1`                              |
| `css` | `string`         | `#rrggbb` when opaque, else `rgba(...)`; ready for `background` |

```tsx
<ColorEditor
  value={color}
  onChange={(c) => {
    c.hex; // '#2f6bff'
    c.rgb; // { r: 47, g: 107, b: 255, a: 1 }
    c.hsl; // { h: 220, s: 100, l: 59, a: 1 }
    c.css; // '#2f6bff'  (or 'rgba(47, 107, 255, 0.8)' when translucent)
    setColor(c.hex);
  }}
/>
```

To run it controlled, **store one field and feed it back to `value`** — the emitted object as a whole is _not_ a valid input. `c.hex` round-trips cleanly for solids; `c.css` round-trips for both solids and gradients, which makes it the simplest choice once gradients are in play.

### Gradient values

With [`gradient`](#gradient) on, both ends widen. The Solid mode still accepts (and emits) the solid forms above; the Gradient mode adds these inputs:

- **`linear-gradient(...)` string** — `'linear-gradient(90deg, #2f6bff 0%, #ec4899 100%)'`. Direction keywords work too (`to right`, `to bottom left`) and are converted to degrees; an omitted angle defaults to `90deg`.
- **Gradient object** — `{ angle: 90, stops: [{ color, position }] }`. `type` is optional (`'linear'`), `angle` defaults to `90`, each `position` (`0–100`) defaults to an even spread, and each stop `color` is itself any `ColorInput` — so channel-set stops work without building a string:

```tsx
// Equivalent gradient inputs — pass whichever form your data is already in:
value = 'linear-gradient(90deg, #2f6bff 0%, #ec4899 100%)';

value = {
  angle: 90,
  stops: [
    { color: { r: 47, g: 107, b: 255, a: 1 }, position: 0 },
    { color: { r: 236, g: 72, b: 153, a: 1 }, position: 100 },
  ],
};
```

The editor edits **exactly two stops** — a gradient input with more keeps only the first and last.

`onChange` then emits a discriminated `ColorEditorValue`. Branch on `type`:

```ts
// Solid mode — the full ColorValue, tagged:
{ type: 'solid', hex, rgb, hsl, hsb, css }

// Gradient mode:
{
  type: 'linear',
  angle: 90,                          // degrees, 0–360
  stops: [                            // always two, sorted by position
    { color: ColorValue, position: 0 },
    { color: ColorValue, position: 100 },
  ],
  css: 'linear-gradient(90deg, …)',   // ready for style={{ background }}
}
```

Each stop's `color` is a full `ColorValue` (`c.stops[0].color.hex`, etc.). `c.css` is a ready `background` string in **both** modes, so storing it is the one-liner that handles solid and gradient alike:

```tsx
const [css, setCss] = useState(
  'linear-gradient(90deg, #2f6bff 0%, #ec4899 100%)',
);

<ColorEditor gradient value={css} onChange={(c) => setCss(c.css)} />;
```

## Examples

### Overview

A solid editor with the full control stack and a row of preset `swatches`. Clicking a swatch applies it to the color; swatches are solid colors only. The value is held in state and read back via `c.hex` in `onChange`.

```tsx
<div className="w-56">
  <ColorEditor
    value={color}
    onChange={(c: ColorValue) => setColor(c.hex)}
    swatches={PALETTE}
  />
</div>
```

### Channel format

`format` picks which channels the scrubber row shows — `'rgb'` (default), `'hsl'`, or `'hsb'`. It only changes the scrubber labels and the numbers the user edits; the emitted value always carries every representation (`rgb`, `hsl`, `hsb`, `hex`, `css`) regardless of which format is on screen.

```tsx
<div className="w-56">
  <ColorEditor
    value={color}
    onChange={(c: ColorValue) => setColor(c.hex)}
    format={format}
  />
</div>
```

### Gradient

Pass `gradient` to enable the Solid/Gradient switch and two-stop linear-gradient editing. In gradient mode the panel gains a stop bar (drag the two thumbs, click to select which stop you're editing), a flip button, and an angle control. `value`/`defaultValue` accept a `linear-gradient()` CSS string or a gradient object, and `onChange` fires a **discriminated** value: `{ type: 'solid', ...ColorValue }` or `{ type: 'linear', angle, stops, css }`. Read `c.css` to get a ready-to-use `background` string for either mode.

```tsx
<div className="w-64">
  <ColorEditor
    gradient
    value={css}
    onChange={(c: ColorEditorValue) => setCss(c.css)}
    swatches={PALETTE}
  />
</div>
```

### Gradient from object stops

A `linear-gradient()` string isn't the only way to supply a gradient. Pass a `{ angle, stops }` object and each stop's `color` is any `ColorInput` — including an `{ r, g, b, a }` (or `{ h, s, l }`) channel set, so you never have to assemble a CSS string by hand. The emitted stops come back the same shape with a full `ColorValue` per stop, so read the rgba straight off `c.stops[i].color.rgb` to keep your state in channel form. Positions are `0–100`; `angle` is in degrees and defaults to `90`.

```tsx
const [angle, setAngle] = useState(90);
const [stops, setStops] = useState([
  { color: { r: 47, g: 107, b: 255, a: 1 }, position: 0 },
  { color: { r: 236, g: 72, b: 153, a: 1 }, position: 100 },
]);

// Pass a { angle, stops } object — each stop color is any ColorInput, here an
// { r, g, b, a } channel set. Every emitted stop carries a full ColorValue, so
// read the rgba straight back off .color.rgb.
<ColorEditor
  gradient
  value={{ angle, stops }}
  onChange={(c) => {
    if (c.type !== 'linear') return;
    setAngle(c.angle);
    setStops(
      c.stops.map((s) => ({ color: s.color.rgb, position: s.position })),
    );
  }}
/>;
```

### Angle as a button

`angleControl` switches the gradient angle widget between a degree `'scrubber'` (default) and a `'button'` that steps the angle 45° per click. The button is the denser choice when you want the gradient bar to take the row and only need coarse direction control.

```tsx
<div className="w-64">
  <ColorEditor
    gradient
    angleControl="button"
    value={css}
    onChange={(c: ColorEditorValue) => setCss(c.css)}
  />
</div>
```

### Compact (area only)

Strip the panel down to just the saturation/brightness area and hue slider by turning off `alpha`, `inputs`, and `hexInput`, and shrink the area with `areaClassName`. Dropped inside a [`Surface`](/react/components/surface/) with a `color`, the picker inherits that accent for its focus ring — useful for a tiny inline picker that lives in a denser inspector row.

```tsx
<Surface outline className="w-56 rounded-3xl" contentClassName="p-4">
  <ColorEditor
    value={color}
    onChange={(c: ColorValue) => setColor(c.hex)}
    alpha={false}
    inputs={false}
    hexInput={false}
    areaClassName="h-24"
  />
</Surface>
```

### Control size

`controlSize` scales the inner controls — scrubbers, hex input, gradient buttons — across `sm`, `md` (default), `lg`, `xl`, and `2xl`. The Solid/Gradient toolbar always renders one step smaller than the rest. The saturation/brightness area and the slider bars keep their fixed height; size them separately via `areaClassName` and `className` if you need to.

```tsx
<div className="w-64">
  <ColorEditor
    value={color}
    onChange={(c: ColorValue) => setColor(c.hex)}
    controlSize={controlSize}
  />
</div>
```

### Control outline

`controlOutline` toggles the surface outline on the inner controls — the channel and alpha scrubbers, the hex input, and the Solid/Gradient toolbar. On by default; turn it off for a flatter, more recessed panel where the controls read as part of the surface rather than as outlined fields. The gradient bar's flip and angle controls stay ghost either way.

```tsx
<div className="w-64">
  <ColorEditor
    value={color}
    onChange={(c: ColorValue) => setColor(c.hex)}
    controlOutline={controlOutline}
  />
</div>
```

### Header & footer

`header` and `footer` take any React node and render above the controls and below the swatches, inside the panel. Use them for the chrome an inspector needs around the picker — a [`SectionTitle`](/react/components/section-title/) and a live hex [`Chip`](/react/components/chip/) up top, an Apply [`Button`](/react/components/button/) at the bottom. Both slots stay interactive when the panel is `disabled`, so they're also the place for an inherit toggle or eyedropper that should keep working while the color controls are dimmed.

```tsx
<Surface outline className="w-64 rounded-3xl" contentClassName="p-4">
  <ColorEditor
    value={color}
    onChange={(c: ColorValue) => setColor(c.hex)}
    swatches={PALETTE}
    header={
      <div className="flex items-center justify-between gap-2">
        <SectionTitle>Fill</SectionTitle>
        <Chip
          size="md"
          className="font-mono font-medium"
          contentClassName="text-xs"
        >
          {color}
        </Chip>
      </div>
    }
    footer={
      <Button color="brand" size="md" rounded className="mt-2 w-full">
        Apply fill
      </Button>
    }
  />
</Surface>
```

### Playground

`controlSize`, `format`, `gradient`, and the `alpha` / `inputs` / `hexInput` toggles compose freely; the surrounding [`Surface`](/react/components/surface/) `color` tints the panel's accent. The hue and alpha bars are the same kind of track you'll find in [`Slider`](/react/components/slider/), and the channel row is a [`NumberScrubber`](/react/components/number-scrubber/) per channel.

```tsx
<Surface
  outline
  color={accent}
  className="w-72 rounded-3xl"
  contentClassName="p-4"
>
  {gradient ? (
    <ColorEditor
      gradient
      value={css}
      onChange={(c: ColorEditorValue) => setCss(c.css)}
      controlSize={controlSize}
      format={format}
      alpha={alpha}
      inputs={inputs}
      hexInput={hexInput}
      controlOutline={controlOutline}
      swatches={swatches ? PALETTE : undefined}
    />
  ) : (
    <ColorEditor
      value={color}
      onChange={(c: ColorValue) => setColor(c.hex)}
      controlSize={controlSize}
      format={format}
      alpha={alpha}
      inputs={inputs}
      hexInput={hexInput}
      controlOutline={controlOutline}
      swatches={swatches ? PALETTE : undefined}
    />
  )}
</Surface>
```

## API Reference

`ColorEditorProps` is a **discriminated union on `gradient`**. With `gradient` omitted or `false`, `value`/`defaultValue` take a solid `ColorInput` and `onChange` fires a `ColorValue`. With `gradient` set to `true`, `value`/`defaultValue` additionally accept a `GradientInput` (a `linear-gradient()` string or gradient object) and `onChange` fires a discriminated `ColorEditorValue` — `{ type: 'solid', ...ColorValue }` or `{ type: 'linear', angle, stops, css }`. `ColorEditorControlSize` is `'sm' | 'md' | 'lg' | 'xl' | '2xl'` and `ColorEditorFormat` is `'rgb' | 'hsl' | 'hsb'`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `alpha?` | `boolean` | `true` | Show the alpha slider and the alpha scrubber. Default `true`. |
| `angleControl?` | `'button' \| 'scrubber'` | `'scrubber'` | Angle control in gradient mode: a 45°-step button, or a degree scrubber. Default `'scrubber'`. |
| `areaClassName?` | `string` | — | Extra classes for the saturation/brightness area (e.g. to set its height). |
| `className?` | `string` | — | Extra classes for the panel root. The panel is full-width — size it via its container. |
| `controlOutline?` | `boolean` | `true` | Render the surface outline on the inner controls — channel/alpha scrubbers, hex input, and the Solid/Gradient toolbar.<br> The gradient bar's flip/angle controls stay ghost regardless. Default `true`. |
| `controlSize?` | `ColorEditorControlSize` | `'md'` | Size of the inner controls — scrubbers, hex input, gradient buttons. Default `'md'`.<br>The Solid/Gradient toolbar is rendered one step smaller. |
| `debounce?` | `number` | `0` | Debounce `onChange` calls in ms. Fires once after changes stop for N ms. Defaults to `0` (immediate). |
| `defaultValue?` | `ColorInput` | — | Initial value (uncontrolled). |
| `disabled?` | `boolean` | — | Dim the panel and block interaction. |
| `footer?` | `React.ReactNode` | — | Content rendered below the panel, after the swatches. Stays interactive when `disabled`. |
| `format?` | `ColorEditorFormat` | `'rgb'` | Which channels the scrubber row shows. Default `'rgb'`. |
| `gradient?` | `false` | `false` | Enable the Solid/Gradient switch and gradient editing. Default `false`. |
| `header?` | `React.ReactNode` | — | Content rendered above the panel, before the controls.<br>Stays interactive when `disabled` (only the panel below dims).<br>For inherit toggles, titles, eyedroppers, etc. |
| `hexInput?` | `boolean` | `true` | Show the hex input. Default `true`. |
| `inputs?` | `boolean` | `true` | Show the channel-scrubber row. Default `true`. |
| `onChange?` | `(value: ColorValue) => void` | — | Fires on every change with the full color. |
| `readOnly?` | `boolean` | — | Block interaction without the dimmed treatment. |
| `swatches?` | `ColorInput[]` | — | Preset colors rendered as a row of thumbs. Clicking one applies it to the color (or the selected gradient stop).<br>Solid colors only. |
| `throttle?` | `number` | `0` | Throttle `onChange` calls in ms. Fires immediately, then at most once per N ms while changing, with a trailing call for the final value. Defaults to `0` (immediate).<br>Takes precedence over `debounce`. |
| `value?` | `ColorInput` | — | Controlled value. A CSS color string or any one channel set. |
