Appica UIUI 1.1.0

Color Picker

A trigger, a popover and a color panel, composed from the color primitives and sharing one value.

Usage

That one line is a whole picker: a swatch button showing the color, and a popover holding a saturation/brightness Color Area, a hue Color Slider and a text input. Nothing is wired up by hand.

But give ColorPicker children and they replace the panel, and any color component inside reads and writes the picker's color through context. No value, no onValueChange, no lifted state:

That works for Color Area, Color Slider, Color Swatch and Color Swatch Picker, plus the two parts that only exist here: ColorPickerInput, a text field that reads any color string the library can parse, and ColorPickerEyeDropper, a button that samples a pixel from the screen. A control that is given its own value opts out of reading the picker's, but still reports what it picks to it.

To control the picker, hold the color yourself:

onValueChange fires on every frame of a drag; onValueCommitted fires once the gesture settles, which is the one to send to a server. Both hand you a Color, whatever you passed in.

Examples

Variants

The default trigger is a Button under the hood, so variant takes three of its styles plus one of its own. flush strips the shell - no padding, no corner, no background - leaving the swatch and the label to line up with whatever sits around them, which is what you want beside a heading or above a readout.

Custom panels

Children replace the panel outright. Here the area is dropped in favour of one slider per channel, with a Select switching color space: the sliders re-key on the space and keep reporting into the same value.

getColorChannels(space) gives the three channels of a space, so the slider list is derived rather than hand-written.

Presets

A Color Swatch Picker inside the panel is a shortcut past the area: click a preset and the area, the slider and the input all move to it. Drag the area afterwards and the preset deselects, because no swatch matches any more.

Opacity

alpha adds an opacity slider to the default panel and moves the format to hexa, so the opacity survives the round trip through the trigger and the input. An alpha format collapses back to its opaque twin whenever the color is fully opaque, so a solid color reads #3b82f6 rather than #3b82f6ff and only grows the last two digits once there is an opacity to show. The trigger's swatch picks up the checkerboard on the same condition.

Eyedropper

eyedropper adds a button that samples a single pixel from anywhere on the screen, using the browser's EyeDropper API. It renders only where that API exists (Chromium today), so a browser without it gets a panel with no dead control rather than a button that does nothing. The sampled color keeps the picker's current opacity.

Inline

inline drops the trigger and the popover and renders the panel in place, for a sidebar or an editor rail. Everything else is unchanged, children included - except that the panel grows a preview Color Swatch, since there is no longer a trigger showing the color.

Custom triggers

label={null} leaves the swatch on its own in a square button - the usual shape in a toolbar. swatchShape and swatchPosition restyle that swatch without replacing it; it is inset from its edge by the same gap the button's height leaves above and below it.

When variant and those two don't reach far enough, trigger takes an element to open the panel from and the picker stops styling anything. The second trigger below is a plain button holding an oversized circular swatch and a label that underlines on hover - a ColorSwatch inside a trigger needs no color, since it reads the picker's.

Name a trigger that has no text with aria-label. The color's description is appended to whatever you pass, so the name is "Stroke color, vivid cyan" rather than a hex string spelled out.

As a form field

A swatch-only picker in an Input's startSlot is the shape most design tools use, and the one that belongs in a form: click the swatch for the panel, or type a value straight into the field. Focusing the field opens the panel too.

name on the picker renders a hidden input carrying the color written in format, so what gets submitted is always a color the library parsed - never the half-typed text in the visible field.

The field is not the popover's trigger, and cannot be. Base UI's trigger puts type="button" on whatever element it renders, or role="button" plus Space and Enter handlers when you tell it the element is not a native button. Either one stops a text box being a text box. So the picker takes trigger={null}, renders no trigger at all, and the field drives open itself.

Four details make that work:

  • trigger={null} leaves the picker as a panel and a hidden input. open becomes required, since nothing else can raise it.
  • popoverProps.anchor points the panel at the field, and initialFocus: false / finalFocus: false leave the caret where it is instead of pulling it into the panel and back. finalFocus matters most: its default returns focus to whatever had it before the panel opened, which here is the field - so closing would re-fire onFocus and reopen on the spot.
  • onOpenChange hands you Base UI's event details, so a press on the text input can cancel() the dismissal it would otherwise cause. Without it, clicking into the text would close the panel that focus just opened.
  • The panel dismisses itself on an outside press, but Escape only reaches it while focus is inside it. The field closes on Escape itself.

The swatch in startSlot is a plain Color Swatch showing the controlled value: decoration, not a control.

Controlled

The value is a Color, not a string, so it carries a color space and an opacity that no hex string could. Convert on the way out with formatColor, which is what the trigger and the input do internally. The trigger is variant="flush" here so its swatch lines up with the readout under it.

Disabled

disabled covers the trigger and every control inside the panel at once, in both modes. Each one takes the treatment the whole family shares: a flat muted fill inside a dashed outline, dimmed.

RTL

Every Appica UI component supports right-to-left layouts out of the box. Set the dir attribute on a container (commonly your <html> element) so CSS logical properties resolve correctly, and wrap your tree in DirectionProvider so direction-aware behavior (roving focus, popup placement, and the like) follows the same direction.

The swatch leads and the label follows, the popover aligns to the start edge (the right), and inside the panel the area and the sliders mirror with ←/→ swapped. Channel values, hex strings and the input stay left-to-right, since they are not language. For setup details and caveats, see the RTL guide.

ألوان اللوحة

API reference

ColorPicker

Every other HTML attribute is forwarded to the trigger, or to the panel when inline. The parts are addressable through data-slot (color-picker, color-picker-trigger, color-picker-panel, color-picker-input, color-picker-eyedropper).

ColorPickerInput

Every other Input prop is forwarded. The field reads any color string parseColor accepts, not only the format it prints, and takes bare hex digits without the #. It must be rendered inside a ColorPicker.

ColorPickerEyeDropper

Every other <button> attribute is forwarded. This is a plain <button> wearing buttonVariants, not the Button component, so the color picker doesn't pull Button into its chunk. It renders null where the browser has no EyeDropper, and must be rendered inside a ColorPicker.

The color model - Color, parseColor, formatColor, convertColor and the channel helpers - ships from @appica/ui-react/color and is documented on the Color Area page.

Accessibility

  • The trigger is a button with aria-haspopup="dialog", and the panel is a labeled dialog: the popover is named from aria-label, or "Color picker" when you don't pass one.
  • The trigger's accessible name ends with a plain-English description of the color - "Brand color, vivid blue" - because a hex string is read out digit by digit. That holds for a swatch-only trigger too, which would otherwise have no name at all.
  • Each control in the panel keeps the accessibility it has on its own page: the area is a pair of sliders with a two-dimensional slider role description, each slider is a labeled range input, and a swatch palette is a listbox.
  • Keyboard: Space/Enter opens the panel and moves focus into it, Tab cycles the controls, Escape closes and returns focus to the trigger. Inside, the arrow keys move by one step and Page Up/Page Down (or a shifted arrow) by a larger one.
  • The text input commits on Enter or on blur, and Escape restores the color. Unparseable text is never committed - the field reverts to the current color.
  • disabled reaches every control through the same context that carries the color, so a disabled picker cannot be operated from the panel either.
  • Nothing depends on color alone: the value is written out as text on the trigger and in the input, and every control is reachable and reportable by keyboard.

© 2026 Appica UI. A free component library, crafted by the Appica team.