Skip to main content
A color picker lets users select colors from a gradient area, channel sliders, or predefined swatches. It builds on the native <input type="color"> experience with a fully customizable, accessible UI.

Features

  • Supports RGBA, HSLA, HSBA, and HEX color formats
  • Color area with draggable thumb for hue/saturation/brightness
  • Per-channel sliders and numeric inputs
  • Predefined color swatches
  • Eyedropper tool (Chrome and Edge only)
  • Keyboard and touch interaction support
  • Form submission and reset support
  • Inline rendering without a trigger popup

Installation

npm install @zag-js/color-picker @zag-js/react

Usage

import * as colorPicker from "@zag-js/color-picker"
import { normalizeProps, useMachine } from "@zag-js/react"
import { useId } from "react"

export function ColorPicker() {
  const service = useMachine(colorPicker.machine, {
    id: useId(),
    defaultValue: colorPicker.parse("#ff0000"),
  })

  const api = colorPicker.connect(service, normalizeProps)

  return (
    <div {...api.getRootProps()}>
      <label {...api.getLabelProps()}>Pick a color</label>
      <div {...api.getControlProps()}>
        <button {...api.getTriggerProps()}>
          <div {...api.getSwatchProps({ value: api.value })} />
        </button>
        <input {...api.getChannelInputProps({ channel: "hex" })} />
      </div>

      <div {...api.getPositionerProps()}>
        <div {...api.getContentProps()}>
          {/* Color area */}
          <div {...api.getAreaProps()}>
            <div {...api.getAreaBackgroundProps()} />
            <div {...api.getAreaThumbProps()} />
          </div>

          {/* Hue slider */}
          <div {...api.getChannelSliderProps({ channel: "hue" })}>
            <div {...api.getChannelSliderTrackProps({ channel: "hue" })} />
            <div {...api.getChannelSliderThumbProps({ channel: "hue" })} />
          </div>

          {/* Alpha slider */}
          <div {...api.getChannelSliderProps({ channel: "alpha" })}>
            <div {...api.getTransparencyGridProps()} />
            <div {...api.getChannelSliderTrackProps({ channel: "alpha" })} />
            <div {...api.getChannelSliderThumbProps({ channel: "alpha" })} />
          </div>

          {/* Channel inputs */}
          <div>
            <input {...api.getChannelInputProps({ channel: "hex" })} />
            <input {...api.getChannelInputProps({ channel: "alpha" })} />
          </div>

          {/* Eyedropper */}
          <button {...api.getEyeDropperTriggerProps()}>Pick</button>
        </div>
      </div>

      {/* Hidden form input */}
      <input {...api.getHiddenInputProps()} />
    </div>
  )
}

Setting the initial color

Use defaultValue with colorPicker.parse(...) to set the starting color. The parse function converts a CSS color string into a Color object.
const service = useMachine(colorPicker.machine, {
  defaultValue: colorPicker.parse("#ff0000"),
})

Controlled color picker

Use value and onValueChange to drive the color externally.
Keep the value as a Color object rather than a string. Converting back and forth between strings can introduce rounding errors.
const [color, setColor] = useState(colorPicker.parse("#ff0000"))

const service = useMachine(colorPicker.machine, {
  value: color,
  onValueChange(details) {
    // details => { value: Color, valueAsString: string }
    setColor(details.value)
  },
})

Color formats

The default output format is rgba. Set format to change it. The value and valueAsString in change callbacks will reflect the active format.
FormatDescription
rgbaRed, Green, Blue, Alpha (default)
hslaHue, Saturation, Lightness, Alpha
hsbaHue, Saturation, Brightness, Alpha
const service = useMachine(colorPicker.machine, {
  format: "hsla",
  onValueChange(details) {
    // details.value is an HSLAColor
    console.log(details.valueAsString) // "hsla(0, 100%, 50%, 1)"
  },
})
Use defaultFormat for uncontrolled initial format, and onFormatChange to react to user-driven format switches:
const service = useMachine(colorPicker.machine, {
  defaultFormat: "hsba",
  onFormatChange(details) {
    console.log(details.format) // "rgba" | "hsla" | "hsba"
  },
})

Channel inputs and sliders

Render individual channel inputs with getChannelInputProps. Match the channels to the active format.
// RGBA channels
<input {...api.getChannelInputProps({ channel: "red" })} />
<input {...api.getChannelInputProps({ channel: "green" })} />
<input {...api.getChannelInputProps({ channel: "blue" })} />
<input {...api.getChannelInputProps({ channel: "alpha" })} />

// Hex input (format-independent)
<input {...api.getChannelInputProps({ channel: "hex" })} />
Render sliders with getChannelSliderProps:
<div {...api.getChannelSliderProps({ channel: "hue" })}>
  <div {...api.getChannelSliderTrackProps({ channel: "hue" })} />
  <div {...api.getChannelSliderThumbProps({ channel: "hue" })} />
</div>

Color swatches

Add preset swatches to help users pick common colors quickly. Use getSwatchTriggerProps for clickable swatches and getSwatchProps for static color previews.
const presets = ["#ff0000", "#00ff00", "#0000ff", "#ffff00"]

<div {...api.getSwatchGroupProps()}>
  {presets.map((color) => (
    <button key={color} {...api.getSwatchTriggerProps({ value: color })}>
      <div {...api.getSwatchProps({ value: color })} />
    </button>
  ))}
</div>
To close the picker after a swatch is selected:
const service = useMachine(colorPicker.machine, {
  closeOnSelect: true,
})

Eyedropper

The eyedropper lets users sample any color from the current page. It is only supported in Chrome and Edge.
<button {...api.getEyeDropperTriggerProps()}>
  Pick color from screen
</button>

Color preview

Show the currently selected color using getSwatchProps with api.value:
<div {...api.getSwatchProps({ value: api.value })} />

{/* Without the alpha channel */}
<div {...api.getSwatchProps({ value: api.value, respectAlpha: false })} />

Positioning the popup

Control popup placement and offset with the positioning prop:
const service = useMachine(colorPicker.machine, {
  positioning: {
    placement: "bottom-start",
    gutter: 8,
  },
})

Inline rendering

Set inline to render the picker body directly in the page without a trigger button or popup container:
const service = useMachine(colorPicker.machine, {
  inline: true,
})

Form usage

Set name to include the color value in form submissions. The machine renders a hidden <input> element automatically.
const service = useMachine(colorPicker.machine, {
  name: "color-preference",
})

// Render the hidden input
<input {...api.getHiddenInputProps()} />

Props

defaultValue
Color
The initial color value when rendered. Use colorPicker.parse(string) to create a Color from a CSS string. Defaults to #000000.
value
Color
The controlled color value. Use with onValueChange for controlled mode.
onValueChange
(details: { value: Color, valueAsString: string }) => void
Fires continuously as the user drags within the color area or sliders.
onValueChangeEnd
(details: { value: Color, valueAsString: string }) => void
Fires when the user releases the pointer after dragging. Useful for debounced updates.
format
"rgba" | "hsla" | "hsba"
The controlled color format. Defaults to rgba.
defaultFormat
"rgba" | "hsla" | "hsba"
The initial color format when uncontrolled. Defaults to rgba.
onFormatChange
(details: { format: ColorFormat }) => void
Called when the user switches color formats.
open
boolean
The controlled open state of the popup.
defaultOpen
boolean
The initial open state when uncontrolled.
onOpenChange
(details: { open: boolean, value: Color }) => void
Called when the popup opens or closes.
closeOnSelect
boolean
Whether the popup closes when a swatch is selected. Defaults to false.
inline
boolean
Render the color picker inline without a trigger and popup wrapper.
disabled
boolean
Disable all user interaction.
readOnly
boolean
Make the color picker read-only.
name
string
The name attribute for the hidden form input.
positioning
PositioningOptions
Popup placement options (e.g. placement, gutter, offset).
initialFocusEl
() => HTMLElement | null
Element to focus when the popup opens.

Styling

Every part of the color picker exposes a data-part attribute. Use these to target parts in CSS.

Parts reference

PartElementDescription
rootdivThe root container
labellabelThe label element
controldivWraps the trigger and text input
triggerbuttonOpens the popup
positionerdivPositions the popup
contentdivThe popup content
areadivThe 2D color gradient area
areaThumbdivDraggable thumb within the area
areaBackgrounddivGradient background of the area
channelSliderdivA single channel slider
channelSliderTrackdivTrack of a channel slider
channelSliderThumbdivThumb of a channel slider
channelInputinputNumeric input for a channel
transparencyGriddivCheckerboard grid for alpha display
swatchGroupdivContainer for swatch triggers
swatchTriggerbuttonA clickable color swatch
swatchdivA color preview element
eyeDropperTriggerbuttonOpens the native eyedropper
formatTriggerbuttonCycles through color formats
formatSelectselectDropdown for color format selection

State attributes

/* Open/closed state on control, trigger, and content */
[data-part="control"][data-state="open"] { }
[data-part="trigger"][data-state="closed"] { }
[data-part="content"][data-state="open"] { }

/* Focus state on control and label */
[data-part="control"][data-focus] { }
[data-part="label"][data-focus] { }

/* Disabled state */
[data-part="label"][data-disabled] { }
[data-part="control"][data-disabled] { }
[data-part="trigger"][data-disabled] { }
[data-part="swatch-trigger"][data-disabled] { }

/* Swatch selected state */
[data-part="swatch-trigger"][data-state="checked"] { }
[data-part="swatch-trigger"][data-state="unchecked"] { }

Accessibility

The color picker follows the WAI-ARIA Color Picker pattern. The color area and sliders use role="slider" with appropriate aria-label, aria-valuemin, aria-valuemax, and aria-valuenow attributes.

Keyboard interactions

KeyDescription
ArrowUpIncrease the value on the focused axis or channel
ArrowDownDecrease the value on the focused axis or channel
ArrowLeftMove left in the color area or decrease the channel value
ArrowRightMove right in the color area or increase the channel value
Shift + ArrowMove in larger steps
Page Up / Page DownIncrease or decrease value by a larger step
Home / EndSet to minimum or maximum value
EnterConfirm a channel input value
EscapeClose the popup
TabMove focus between interactive parts

Build docs developers (and LLMs) love