Input

Color picker

A flower of overlapping discs that blooms out of the swatch you clicked. Three rings choose hue and saturation, a band takes the current colour, and an arc hangs off the right edge for lightness.

Usage

import { ColorPicker } from '@carabine/ui/color-picker';
#FC8835
const [color, setColor] = useState('#FC8835');

<ColorPicker value={color} onValueChange={setColor} />

Any hex in, six uppercase digits out. One shape of value means nobody downstream has to normalise it before comparing two colours.

How it blooms

The petals overlap on purpose — a ring of tangent circles reads as a dial, a ring of overlapping ones reads as a flower — and each arrives on its own spring with stagger milliseconds added per petal, so the bloom has a direction rather than appearing all at once.

The spring is 520 stiffness at 31 damping with a mass of 0.62. Critical damping is 2 × √stiffness, so about 46 — this is light and well past critical, and the overshoot is what makes a petal arrive rather than slide in.

One thing here is CSS rather than React: the hovered petal's z-index. A petal that grows under its neighbours looks broken, and re-ordering the DOM on hover would move focus. color-picker.css owns it, because that is the one property React must not.

Examples

Your own flower

rings replaces rather than merges: a colour set is a whole, not a table of variants to patch. A ring with a single colour sits in the middle whatever its radius says, and a swatch with no label is named by its colour string — the one label that never needs translating.

<ColorPicker
  size={132}
  rings={[
    { radius: 0, colors: ['#FFFFFF'], label: () => 'White' },
    { radius: 0.18, colors: ['#F4F4F5', '#D4D4D8', '#A1A1AA', '#71717A'] },
    { radius: 0.34, colors: brand, rotate: 15 },
  ]}
/>

Customization

One number drives the geometry. size is the disc's diameter and the petal, the band, the arc and the knob are all fractions of it — so the whole flower scales by changing one prop rather than six.

<ColorPicker
  size={140}
  petal={32}            // they overlap on purpose
  band={13}             // the ring that takes the current colour
  plate={false}         // no blurred plate under the petals
  arcSpread={70}        // degrees of lightness arc, centred on the right edge
  minLightness={10}     // the slice of HSL lightness the arc maps to
  maxLightness={90}
  triggerSize={22}
  theme="light"
/>

Styling reference

The picker is the toast's material at another size — a glass shell with a lit core — and plate is what puts it under the petals. Turn it off and the flower floats on the page, which is right when it is already inside a panel of your own.

PropWhat it moves
stiffness / damping / massThe bloom, and a petal answering a hover
staggerMilliseconds added per petal on the way in
openStiffness / openDampingThe panel opening and closing
openScaleThe scale the panel grows from — 0.42, and it grows from the trigger

API reference

PropType
classNamestringAdded to the panel’s own classes, so yours wins
styleCSSPropertiesMerged after the component’s own inline styles, so yours wins

Both land on the panel. The trigger is already your element — you style it where you write it — and the panel is the part that portals away from your markup and is otherwise out of reach. State is legible from CSS as well — data-state, data-disabled, data-side — so a rule can answer it without knowing a single class name of ours. See Theming.

PropTypeDefault
valuestringControlled. Any hex in, six uppercase digits out
defaultValuestring'#FC8835'
onValueChange(value: string) => void
onOpenChange(open: boolean) => void
ringsColorRing[]defaultRingsReplaces the palette outright
sizenumber107Diameter of the disc. Everything else follows it
petalnumbersize × 0.224
bandnumbersize × 0.0896The ring that takes the current colour
platebooleantrueA blurred plate under the petals
arcRadiusnumbersize × 0.7531From the disc’s centre
arcSpreadnumber50Degrees, centred on the right edge
arcWidthnumbersize × 0.0933
knobnumbersize × 0.1306
minLightnessnumber0The slice of HSL lightness the arc maps to
maxLightnessnumber100
triggerSizenumber18
offsetnumber12Smallest gap kept from the viewport’s edges
zIndexnumber50
containerHTMLElement | nulldocument.bodyWhere it portals to
disabledbooleanfalse
labelstring'Color'Names the trigger and the panel
theme'dark' | 'light''dark'