Color Picker

A compact swatch trigger that opens a ColorEditor in a Popover. Reach for it in dense inspectors and property panels where a full inline editor would take up too much room.

<ColorPicker
  value={value}
  onChange={(c) => setValue(c.hex)}
  className="w-48"
/>

Usage

import { ColorPicker } from '@cladd-ui/react';
 
function Example() {
  const [value, setValue] = useState('#3b82f6');
 
  return <ColorPicker value={value} onChange={(c) => setValue(c.hex)} />;
}

Values

ColorPicker takes the same value props as the ColorEditor it wraps. value / defaultValue accept any ColorInput — a CSS string ('#2f6bff', 'rgba(...)', 'red') or a single channel set ({ r, g, b }, { h, s, l }, { h, s, b }) — and onChange emits the fully-resolved ColorValue (hex, rgb, hsl, hsb, css). With gradient, input also takes a linear-gradient() string or gradient object and onChange emits the discriminated solid / linear value. The editor's Values section has the full breakdown of accepted formats, ranges, and emitted fields.

Two behaviors are specific to the picker's trigger:

  • Label. A solid value shows its uppercased hex (#2F6BFF); a gradient shows gradientLabel (default 'Gradient'). Pass children to render your own value node instead.
  • Empty state. A missing value (null, undefined, or '') — or a fully transparent one (alpha 0, including a gradient whose every stop is transparent) — collapses the swatch to an outlined "×" and shows placeholder in place of the label.

Examples

Empty

When there is no value — null, an empty string, or a fully transparent color — the swatch collapses to an outlined "×" and the trigger shows the placeholder node in place of a hex label.

<ColorPicker
  value={value}
  placeholder="No fill"
  onChange={(c) => setValue(c.hex)}
  className="w-48"
/>

Gradient

Set gradient to add a Solid / Gradient switch and let the embedded ColorEditor edit a linear gradient. This flips the value to the discriminated union — onChange now emits a solid or linear value, so read c.css for the CSS string rather than c.hex. Pass swatches to offer preset stop colors.

<ColorPicker
  gradient
  value={value}
  swatches={PALETTE}
  onChange={(c) => setValue(c.css)}
  className="w-48"
/>

Custom value display

Pass children to replace the auto-formatted hex / gradient label with your own value row — here the hex with the field name pushed to the trailing edge — and set a leading glyph through the icon prop. This is the inspector-row pattern used in tools like PaneFlow.

<ColorPicker
  value={value}
  onChange={(c) => setValue(c.hex)}
  icon={<Fill className="size-4" />}
  className="w-56"
>
  <span className="flex w-full items-center gap-2">
    <span className="font-mono uppercase">{value}</span>
    <span className="ml-auto text-cladd-fg-softer">Fill</span>
  </span>
</ColorPicker>

Sizes

size runs the standard 2xs → 2xl scale shared with Button, Select, and the rest of the size-aware controls — the swatch scales with the trigger. Pass the same token you'd use on a sibling control and the picker sits at the matching height. The second row pairs rounded and outline to read as a pill that matches a recessed form. The editor inside the popover is unaffected — size it independently with controlSize.

<ColorPicker
  size="md"
  value={value}
  onChange={(c) => setValue(c.hex)}
  className="w-40"
/>
<ColorPicker
  size="md"
  rounded
  outline
  value={value}
  onChange={(c) => setValue(c.hex)}
  className="w-40"
/>

In a toolbar

ColorPickers slot into a Toolbar as a property row — a fill swatch, a ToolbarSeparator, and a stroke swatch — for the dense inspector feel cladd was built for.

Fill
Stroke
<Toolbar aria-label="Shape style">
  <span className="px-2 text-xs font-semibold text-cladd-fg-soft">
    Fill
  </span>
  <ColorPicker
    rounded
    value={fill}
    onChange={(c) => setFill(c.hex)}
    className="w-32"
  />
  <ToolbarSeparator />
  <span className="px-2 text-xs font-semibold text-cladd-fg-soft">
    Stroke
  </span>
  <ColorPicker
    rounded
    value={stroke}
    onChange={(c) => setStroke(c.hex)}
    className="w-32"
  />
</Toolbar>

header and footer render your own nodes above and below the editor, inside the popover — the chrome a real picker needs around the swatches. Here a SectionTitle and a live hex Chip label the popover, and an "Apply fill" Button in the footer commits and closes it via the controlled popoverState.

<ColorPicker
  value={value}
  onChange={(c) => setValue(c.hex)}
  swatches={PALETTE}
  className="w-48"
  popoverState={open}
  onPopoverState={setOpen}
  header={
    <div className="mb-2 flex items-center justify-between gap-2">
      <SectionTitle>Fill</SectionTitle>
      <Chip
        size="md"
        className="font-mono font-medium"
        contentClassName="text-xs"
      >
        {value}
      </Chip>
    </div>
  }
  footer={
    <Button
      color="brand"
      size="md"
      rounded
      className="mt-2 w-full"
      onClick={() => setOpen(false)}
    >
      Apply fill
    </Button>
  }
/>

Playground

size, color, rounded, outline, and gradient compose freely — toggle between a solid and a gradient picker and restyle the trigger to match its surroundings.

{gradient ? (
  <ColorPicker
    gradient
    size="md"
    color="brand"
    rounded={false}
    outline={false}
    swatches={PALETTE}
    value={grad}
    onChange={(c) => setGrad(c.css)}
    className="w-48"
  />
) : (
  <ColorPicker
    size="md"
    color="brand"
    rounded={false}
    outline={false}
    swatches={PALETTE}
    value={solid}
    onChange={(c) => setSolid(c.hex)}
    className="w-48"
  />
)}

API Reference

ColorPicker is a compact swatch trigger that opens a ColorEditor inside a Popover. The value props form a discriminated union on gradient: by default they describe a solid color (ColorValue), and with gradient set they describe a gradient (GradientValue). Editor props such as alpha, inputs, format, swatches, and controlSize are forwarded to the embedded ColorEditor.

Inherits from ButtonProps
alpha: booleantrueShow the alpha slider and scrubber. Default true.
anchorRef: RefObject<HTMLElement | null>External anchor ref. When provided the trigger button is not rendered - the caller owns the trigger and popoverState wiring.
angleControl: 'button' | 'scrubber''scrubber'Gradient angle control: a 45°-step button, or a degree scrubber. Default 'scrubber'.
areaClassName: stringExtra classes for the editor's saturation/brightness area.
children: ReactNodeCustom node rendered in place of the auto-formatted value (hex / gradient label).

Rendered as data-part="value" — use to show your own value text, a field label, or richer formatting.
className: stringExtra classes for the trigger button.
color: ColorAccent color for the trigger button. Forwarded to Button.color.
contentClassName: stringExtra classes for the trigger button's inner content row.
controlOutline: booleantrueRender the surface outline on the editor's inner controls. Default true.
controlSize: ColorEditorControlSize'md'Size of the editor's inner controls. Default 'md'.
debounce: numberDebounce onChange calls in ms. Forwarded to the editor.
defaultValue: ColorInputInitial value (uncontrolled).
disabled: booleanVisually dim the trigger and prevent the popover from opening.
dropdownIcon: booleantrueShow the chevron-down indicator on the right of the trigger. Default true.
editorClassName: stringExtra classes for the editor panel root.
footer: ReactNodeContent rendered below the editor panel, inside the popover.
format: ColorEditorFormat'rgb'Which channels the scrubber row shows. Default 'rgb'.
gradient: falsefalseEnable the Solid/Gradient switch and gradient editing. Default false.
gradientLabel: string'Gradient'Label rendered for a gradient value in the trigger. Default 'Gradient'.
header: ReactNodeContent rendered above the editor panel, inside the popover.
hexInput: booleantrueShow the hex input. Default true.
icon: ReactNodeIcon node rendered at the start of the trigger, before the swatch.
iconClassName: stringExtra classes for the icon wrapper.
inputs: booleantrueShow the channel-scrubber row. Default true.
multiline: booleanForwarded to the trigger Button — allows wrapping the value across lines.
onChange: (value: ColorValue) => voidFires on every change with the full color.
onClick: (e: MouseEvent) => voidFires when the trigger button is clicked (before the popover toggles).
onPopoverState: (state: boolean) => voidFires whenever the popover open state changes.
outline: booleanRender the trigger button's surface outline ring. Forwarded to Button.outline.
placeholder: ReactNodeValue shown when there is no color (or a fully transparent one) and no children.
placeholderClassName: stringExtra classes applied to the value container when showing the placeholder.
popoverClassName: string'w-64 p-3'Extra classes for the popover. Default 'w-64 p-3'.
popoverColor: ColorAccent color for the popover. Forwarded to Popover.color.
popoverOffset: PopoverOffset['-50%', 4]Default ['-50%', 4].
popoverPosition: PopoverPosition'bottom-end'Default 'bottom-end'.
popoverState: booleanControlled popover open state. Pair with onPopoverState.
popoverSurfaceLevel: number | stringSurface level for the popover. Default same as Popover's surfaceLevel.
readOnly: booleanShow the trigger with the current value but block opening the popover.
reverse: booleanReverse the visual order of the content row (icon/swatch/value ↔ dropdown).
rounded: booleanPill-style trigger button. Forwarded to Button.rounded.
size: ButtonSizeTrigger button size — also scales the swatch. Forwarded to Button.size.
surface: 'surface' | 'cut'Trigger button surface: 'surface' (default) or 'cut' (inset/recessed).
swatchClassName: stringExtra classes for the color swatch.
swatches: ColorInput[]Preset colors rendered as a row of thumbs in the editor.
throttle: numberThrottle onChange calls in ms. Forwarded to the editor.
value: ColorInputControlled value. A CSS color string or any one channel set.
valueClassName: stringExtra classes for the value display inside the trigger.