Color Picker
The color picker is an input widget used to select a color value from a predefined list or a color area.
This component builds on top of the native <input type=color>
experience and
provides a more customizable and consistent user experience.
Features
- Support for custom color area
- Support for RGBA, HSLA, HEX, and HSBA formats
- Support for channel inputs and sliders
- Support for mouse, touch, and keyboard interactions
- Support for form submission and reset events
- Support for named css colors
Installation
To use the combobox machine in your project, run the following command in your command line:
npm install @zag-js/color-picker @zag-js/react # or yarn add @zag-js/color-picker @zag-js/react
npm install @zag-js/color-picker @zag-js/solid # or yarn add @zag-js/color-picker @zag-js/solid
npm install @zag-js/color-picker @zag-js/vue # or yarn add @zag-js/color-picker @zag-js/vue
npm install @zag-js/color-picker @zag-js/vue # or yarn add @zag-js/color-picker @zag-js/vue
This command will install the framework agnostic combobox logic and the reactive utilities for your framework of choice.
Anatomy
To set up the combobox correctly, you'll need to understand its anatomy and how we name its parts.
Each part includes a
data-part
attribute to help identify them in the DOM.
Usage
First, import the combobox package into your project
import * as colorPicker from "@zag-js/color-picker"
The color picker package exports these functions:
machine
— The state machine logic for the color picker widget.connect
— The function that translates the machine's state to JSX attributes and event handlers.parse
- The function that parses a color string to an Color object.
Next, import the required hooks and functions for your framework and use the color picker machine in your project 🔥
import * as colorPicker from "@zag-js/color-picker" import { normalizeProps, useMachine } from "@zag-js/react" import { useId } from "react" function ColorPicker() { const [state, send] = useMachine( colorPicker.machine({ id: useId(), value: colorPicker.parse("hsl(0, 100%, 50%)"), }), ) const api = colorPicker.connect(state, send, normalizeProps) return ( <div {...api.rootProps}> <label {...api.labelProps}>Select Color: {api.valueAsString}</label> <input {...api.hiddenInputProps} /> <div {...api.controlProps}> <button {...api.triggerProps}> <div {...api.getTransparencyGridProps({ size: "10px" })} /> <div {...api.getSwatchProps({ value: api.value })} /> </button> <input {...api.getChannelInputProps({ channel: "hex" })} /> <input {...api.getChannelInputProps({ channel: "alpha" })} /> </div> <div {...api.positionerProps}> <div {...api.contentProps}> <div {...api.getAreaProps()}> <div {...api.getAreaBackgroundProps()} /> <div {...api.getAreaThumbProps()} /> </div> <div {...api.getChannelSliderProps({ channel: "hue" })}> <div {...api.getChannelSliderTrackProps({ channel: "hue" })} /> <div {...api.getChannelSliderThumbProps({ channel: "hue" })} /> </div> <div {...api.getChannelSliderProps({ channel: "alpha" })}> <div {...api.getTransparencyGridProps({ size: "12px" })} /> <div {...api.getChannelSliderTrackProps({ channel: "alpha" })} /> <div {...api.getChannelSliderThumbProps({ channel: "alpha" })} /> </div> </div> </div> </div> ) }
import * as colorPicker from "@zag-js/color-picker" import { normalizeProps, useMachine } from "@zag-js/solid" import { createMemo, createUniqueId } from "solid-js" function ColorPicker() { const [state, send] = useMachine( colorPicker.machine({ id: createUniqueId(), value: colorPicker.parse("hsl(0, 100%, 50%)"), }), ) const api = createMemo(() => colorPicker.connect(state, send, normalizeProps)) return ( <div {...api().rootProps}> <label {...api().labelProps}>Select Color: {api.valueAsString}</label> <input {...api().hiddenInputProps} /> <div {...api().controlProps}> <button {...api().triggerProps}> <div {...api().getTransparencyGridProps({ size: "10px" })} /> <div {...api().getSwatchProps({ value: api.value })} /> </button> <input {...api().getChannelInputProps({ channel: "hex" })} /> <input {...api().getChannelInputProps({ channel: "alpha" })} /> </div> <div {...api().positionerProps}> <div {...api().contentProps}> <div {...api().getAreaProps()}> <div {...api().getAreaBackgroundProps()} /> <div {...api().getAreaThumbProps()} /> </div> <div {...api().getChannelSliderProps({ channel: "hue" })}> <div {...api().getChannelSliderTrackProps({ channel: "hue" })} /> <div {...api().getChannelSliderThumbProps({ channel: "hue" })} /> </div> <div {...api().getChannelSliderProps({ channel: "alpha" })}> <div {...api().getTransparencyGridProps({ size: "12px" })} /> <div {...api().getChannelSliderTrackProps({ channel: "alpha" })} /> <div {...api().getChannelSliderThumbProps({ channel: "alpha" })} /> </div> </div> </div> </div> ) }
Setting the initial value
To set the initial value of the color picker, use the value
context property.
const [current, send] = useMachine( colorPicker.machine({ value: colorPicker.parse("#ff0000"), }), )
Listening for change events
When the user selects a color using the color picker, the onValueChange
and
onValueChangeEnd
events will be fired.
onValueChange
— Fires in sync as the user selects a coloronValueChangeEnd
— Fires when the user stops selecting a color (useful for debounced updates)
const [current, send] = useMachine( colorPicker.machine({ onValueChange: (details) => { // details => { value: Color, valueAsString: string } }, onValueChangeEnd: (details) => { // details => { value: Color, valueAsString: string } }, }), )
When using the
onValueChange
method in React.js, you might need to use theflushSync
method fromreact-dom
to ensure the value is updated in sync
Using a custom color format
By default, the color picker's output format is rgba
. You can change this
format to either hsla
or hsba
by using the format
context property.
When this property is set, the value
and valueAsString
properties of the
onValueChange
event will be updated to reflect the new format.
const [current, send] = useMachine( colorPicker.machine({ format: "hsla", onValueChange: (details) => { // details => { value: HSLAColor, valueAsString: string } }, }), )
Showing color presets
Adding color presets in form of swatches can help users pick colors faster. To
support this, use the getSwatchTriggerProps(...)
and getSwatchProps(...)
to
get the props needed to show the swatches buttons.
const ColorPicker = () => { const [state, send] = useMachine( colorPicker.machine({ id: useId(), value: colorPicker.parse("hsl(0, 100%, 50%)"), }), ) const api = colorPicker.connect(state, send, normalizeProps) const presets = ["#ff0000", "#00ff00", "#0000ff"] return ( <div {...api.rootProps}> {/* ... */} <div {...api.positionerProps}> <div {...api.contentProps}> <div {...api.swatchGroupProps}> {presets.map((preset) => ( <button key={preset} {...api.getSwatchTriggerProps({ value: preset })} > <div style={{ position: "relative" }}> <div {...api.getTransparencyGridProps({ size: "4px" })} /> <div {...api.getSwatchProps({ value: preset })} /> </div> </button> ))} </div> </div> </div> </div> ) }
const ColorPicker = () => { const [state, send] = useMachine( colorPicker.machine({ id: createUniqueId(), value: colorPicker.parse("hsl(0, 100%, 50%)"), }), ) const api = createMemo(() => colorPicker.connect(state, send, normalizeProps)) const presets = ["#ff0000", "#00ff00", "#0000ff"] return ( <div {...api().rootProps}> {/* ... */} <div {...api().positionerProps}> <div {...api().contentProps}> <div {...api().swatchGroupProps}> <Index each={preset}> {(preset) => ( <button {...api().getSwatchTriggerProps({ value: preset() })}> <div style={{ position: "relative" }}> <div {...api().getTransparencyGridProps({ size: "4px" })} /> <div {...api().getSwatchProps({ value: preset() })} /> </div> </button> )} </Index> </div> </div> </div> </div> ) }
Disabling the color picker
To disable user interactions with the color picker, set the disabled
context
property to true
.
const [current, send] = useMachine( colorPicker.machine({ disabled: true, }), )
Controlling the open and closed state
To control the open and closed state of the color picker, use the open
and
onOpenChange
context properties.
const [current, send] = useMachine( colorPicker.machine({ open: true, onOpenChange: (details) => { // details => { open: boolean } }, }), )
You can also leverage the api.open()
or api.close()
methods to control the
open and closed state of the color picker.
Controlling individual color channel
In some cases, you may want to allow users to control the values of each color channel individually. You can do this using an input element or a slider element, or both.
To support this, use the getChannelInputProps(...)
to show the channel inputs.
Note: Make sure you only render the channel inputs that match the
format
of the color picker.
const ColorPicker = () => { const [state, send] = useMachine( colorPicker.machine({ id: useId(), value: colorPicker.parse("hsl(0, 100%, 50%)"), }), ) const api = colorPicker.connect(state, send, normalizeProps) return ( <div {...api.rootProps}> {/* ... */} <div {...api.positionerProps}> <div {...api.contentProps}> {api.format === "rgba" && ( <div> <div> <span>R</span> <input {...api.getChannelInputProps({ channel: "red" })} /> </div> <div> <span>G</span> <input {...api.getChannelInputProps({ channel: "green" })} /> </div> <div> <span>B</span> <input {...api.getChannelInputProps({ channel: "blue" })} /> </div> <div> <span>A</span> <input {...api.getChannelInputProps({ channel: "alpha" })} /> </div> </div> )} </div> </div> </div> ) }
import { Show } from "solid-js" const ColorPicker = () => { const [state, send] = useMachine( colorPicker.machine({ id: createUniqueId(), value: colorPicker.parse("hsl(0, 100%, 50%)"), }), ) const api = createMemo(() => colorPicker.connect(state, send, normalizeProps)) return ( <div {...api().rootProps}> {/* ... */} <div {...api().positionerProps}> <div {...api().contentProps}> <Show when={api().format === "rgba"}> <div> <div> <span>R</span> <input {...api().getChannelInputProps({ channel: "red" })} /> </div> <div> <span>G</span> <input {...api().getChannelInputProps({ channel: "green" })} /> </div> <div> <span>B</span> <input {...api().getChannelInputProps({ channel: "blue" })} /> </div> <div> <span>A</span> <input {...api().getChannelInputProps({ channel: "alpha" })} /> </div> </div> </Show> </div> </div> </div> ) }
Showing a color preview
To display the value of a color, use the getSwatchProps(...)
and pass the
color value. To show the current color value, use the api.value
const ColorPicker = () => { const [state, send] = useMachine( colorPicker.machine({ id: useId(), value: colorPicker.parse("hsl(0, 100%, 50%)"), }), ) const api = colorPicker.connect(state, send, normalizeProps) return ( <div {...api.rootProps}> <div> <div {...api.getTransparencyGridProps({ size: "4px" })} /> <div {...api.getSwatchProps({ value: api.value })} /> </div> {/* ... */} </div> ) }
const ColorPicker = () => { const [state, send] = useMachine( colorPicker.machine({ id: createUniqueId(), value: colorPicker.parse("hsl(0, 100%, 50%)"), }), ) const api = createMemo(() => colorPicker.connect(state, send, normalizeProps)) return ( <div {...api().rootProps}> <div> <div {...api().getTransparencyGridProps({ size: "4px" })} /> <div {...api().getSwatchProps({ value: api().value })} /> </div> {/* ... */} </div> ) }
You can pass
respectAlpha: false
to show the color value without the alpha channel
Adding a eyedropper
The eye dropper tool is a native browser feature that allows a user pick a color
from a current page's canvas. To support this, use the
getEyeDropperTriggerProps(...)
.
Note: The eye dropper tool only works in Chrome and Edge browsers
const ColorPicker = () => { const [state, send] = useMachine( colorPicker.machine({ id: useId(), value: colorPicker.parse("hsl(0, 100%, 50%)"), }), ) const api = colorPicker.connect(state, send, normalizeProps) return ( <div {...api.rootProps}> {/* ... */} <div {...api.positionerProps}> <div {...api.contentProps}> <button {...api.eyeDropperTriggerProps}> <EyeDropIcon /> </button> </div> </div> </div> ) }
const ColorPicker = () => { const [state, send] = useMachine( colorPicker.machine({ id: createUniqueId(), value: colorPicker.parse("hsl(0, 100%, 50%)"), }), ) const api = createMemo(() => colorPicker.connect(state, send, normalizeProps)) return ( <div {...api().rootProps}> {/* ... */} <div {...api().positionerProps}> <div {...api().contentProps}> <button {...api().eyeDropperTriggerProps}> <EyeDropIcon /> </button> </div> </div> </div> ) }
Usage within forms
To use the color picker within a form, add the name
context property to the
machine and render the visually hidden input using the hiddenInputProps
.
const [state, send] = useMachine( combobox.machine({ name: "color-preference", }), )
Styling guide
Each color picker part has a data-part
attribute added to them to help you
identify and style them easily.
Open and closed state
When the color picker is open or closed, the data-state
attribute is added to
the trigger, content, control parts.
[data-part="control"][data-state="open|closed"] { /* styles for control open or state */ } [data-part="trigger"][data-state="open|closed"] { /* styles for control open or state */ } [data-part="content"][data-state="open|closed"] { /* styles for control open or state */ }
Focused State
When the combobox is focused, the data-focus
attribute is added to the control
and label parts.
[data-part="control"][data-focus] { /* styles for control focus state */ } [data-part="label"][data-focus] { /* styles for label focus state */ }
Disabled State
When the combobox is disabled, the data-disabled
attribute is added to the
label, control, trigger and option parts.
[data-part="label"][data-disabled] { /* styles for label disabled state */ } [data-part="control"][data-disabled] { /* styles for control disabled state */ } [data-part="trigger"][data-disabled] { /* styles for trigger disabled state */ } [data-part="swatch-trigger"][data-disabled] { /* styles for item disabled state */ }
Swatch State
When a swatch's color value matches the color picker's value, the
data-state=checked
attribute is added to the swatch part.
[data-part="swatch-trigger"][data-state="checked|unchecked"] { /* styles for swatch's checked state */ }
Methods and Properties
Machine Context
The color picker machine exposes the following context properties:
ids
Partial<{ root: string; control: string; trigger: string; label: string; input: string; content: string; area: string; areaGradient: string; areaThumb: string; channelInput(id: string): string; channelSliderTrack(id: ColorChannel): string; }>
The ids of the elements in the color picker. Useful for composition.dir
"ltr" | "rtl"
The direction of the color pickervalue
Color
The current color valuedisabled
boolean
Whether the color picker is disabledreadOnly
boolean
Whether the color picker is read-onlyonValueChange
(details: ValueChangeDetails) => void
Handler that is called when the value changes, as the user drags.onValueChangeEnd
(details: ValueChangeDetails) => void
Handler that is called when the user stops dragging.onOpenChange
(details: OpenChangeDetails) => void
Handler that is called when the user opens or closes the color picker.name
string
The name for the form inputpositioning
PositioningOptions
The positioning options for the color pickerinitialFocusEl
MaybeFunction<HTMLElement>
The initial focus element when the color picker is opened.open
boolean
Whether the color picker is openformat
ColorFormat
The color format to useonFormatChange
(details: FormatChangeDetails) => void
Function called when the color format changescloseOnSelect
boolean
Whether to close the color picker when a swatch is selectedid
string
The unique identifier of the machine.getRootNode
() => Node | ShadowRoot | Document
A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.onPointerDownOutside
(event: PointerDownOutsideEvent) => void
Function called when the pointer is pressed down outside the componentonFocusOutside
(event: FocusOutsideEvent) => void
Function called when the focus is moved outside the componentonInteractOutside
(event: InteractOutsideEvent) => void
Function called when an interaction happens outside the component
Machine API
The color picker api
exposes the following methods:
isDragging
boolean
Whether the color picker is being draggedisOpen
boolean
Whether the color picker is openvalue
Color
The current color value (as a string)valueAsString
string
The current color value (as a Color object)setValue
(value: string | Color) => void
Function to set the color valuegetChannelValue
(channel: ColorChannel) => string
Function to set the color valuesetChannelValue
(channel: ColorChannel, value: number) => void
Function to set the color value of a specific channelformat
ColorFormat
The current color formatsetFormat
(format: ColorFormat) => void
Function to set the color formatalpha
number
The alpha value of the colorsetAlpha
(value: number) => void
Function to set the color alphaopen
() => void
Function to open the color pickerclose
() => void
Function to close the color picker
Edit this page on GitHub