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
Install
Install the component from your command line.
Anatomy
Import all parts and piece them together.
<script setup lang="ts">import * as colorPicker from '@destyler/color-picker'import { normalizeProps, useMachine } from '@destyler/vue'import { computed, useId } from 'vue'
const [state, send] = useMachine(colorPicker.machine({ id: useId(), value: colorPicker.parse('hsl(240,5.9%,10%)'),}))
const api = computed(() => colorPicker.connect(state.value, send, normalizeProps))</script>
<template> <div v-bind="api.getRootProps()"> <input v-bind="api.getHiddenInputProps()"> <div v-bind="api.getControlProps()"> <button v-bind="api.getTriggerProps()" > <div v-bind="api.getTransparencyGridProps({ size: '8px' })" /> <div v-bind="api.getSwatchProps({ value: api.value })" /> </button> <input v-bind="api.getChannelInputProps({ channel: 'hex' })"> <input v-bind="api.getChannelInputProps({ channel: 'alpha' })"> </div>
<Teleport v-if="api.open" to="body"> <div v-bind="api.getPositionerProps()" > <div v-bind="api.getContentProps()" > <div v-bind="api.getAreaProps()" > <div v-bind="api.getAreaBackgroundProps()" /> <div v-bind="api.getAreaThumbProps()" /> </div>
<div v-bind="api.getChannelSliderProps({ channel: 'hue' })" > <div v-bind="api.getChannelSliderTrackProps({ channel: 'hue' })" /> <div v-bind="api.getChannelSliderThumbProps({ channel: 'hue' })" /> </div>
<div v-bind="api.getChannelSliderProps({ channel: 'alpha' })" > <div v-bind="api.getTransparencyGridProps({ size: '8px' })" /> <div v-bind="api.getChannelSliderTrackProps({ channel: 'alpha' })" /> <div v-bind="api.getChannelSliderThumbProps({ channel: 'alpha' })" /> </div> </div> </div> </Teleport> </div></template>
import * as colorPicker from '@destyler/color-picker'import { normalizeProps, useMachine } from '@destyler/react'import { useId } from 'react'import { createPortal } from 'react-dom'
export default function ColorPicker() { const id = useId() const [state, send] = useMachine(colorPicker.machine({ id, value: colorPicker.parse('hsl(240,5.9%,10%)'), }))
const api = colorPicker.connect(state, send, normalizeProps)
return ( <div {...api.getRootProps()} > <input {...api.getHiddenInputProps()} /> <div {...api.getControlProps()} > <button {...api.getTriggerProps()}> <div {...api.getTransparencyGridProps({ size: '8px' })} /> <div {...api.getSwatchProps({ value: api.value })} /> </button> <input {...api.getChannelInputProps({ channel: 'hex' })}/> <input {...api.getChannelInputProps({ channel: 'alpha' })}/> </div>
{api.open && createPortal( <div {...api.getPositionerProps()} > <div {...api.getContentProps()} > <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: '8px' })} /> <div {...api.getChannelSliderTrackProps({ channel: 'alpha' })} /> <div {...api.getChannelSliderThumbProps({ channel: 'alpha' })} /> </div> </div> </div>, document.body )} </div> )}
<script lang="ts"> import * as colorPicker from '@destyler/color-picker' import { normalizeProps, useMachine, portal } from '@destyler/svelte'
const id = $props.id()
const [state, send] = useMachine(colorPicker.machine({ id, value: colorPicker.parse('hsl(240,5.9%,10%)'), }))
const api = $derived(colorPicker.connect(state, send, normalizeProps))</script>
<div {...api.getRootProps()}> <input {...api.getHiddenInputProps()}> <div {...api.getControlProps()}> <button {...api.getTriggerProps()}> <div {...api.getTransparencyGridProps({ size: '8px' })} ></div> <div {...api.getSwatchProps({ value: api.value })} ></div> </button> <input {...api.getChannelInputProps({ channel: 'hex' })}> <input {...api.getChannelInputProps({ channel: 'alpha' })}> </div>
{#if api.open} <div use:portal> <div {...api.getPositionerProps()}> <div {...api.getContentProps()} > <div {...api.getAreaProps()} > <div {...api.getAreaBackgroundProps()} ></div> <div {...api.getAreaThumbProps()} ></div> </div>
<div {...api.getChannelSliderProps({ channel: 'hue' })} > <div {...api.getChannelSliderTrackProps({ channel: 'hue' })} ></div> <div {...api.getChannelSliderThumbProps({ channel: 'hue' })} ></div> </div>
<div {...api.getChannelSliderProps({ channel: 'alpha' })} > <div {...api.getTransparencyGridProps({ size: '8px' })} ></div> <div {...api.getChannelSliderTrackProps({ channel: 'alpha' })} ></div> <div {...api.getChannelSliderThumbProps({ channel: 'alpha' })} ></div> </div> </div> </div> </div> {/if}</div>
import * as colorPicker from '@destyler/color-picker'import { normalizeProps, useMachine } from '@destyler/solid'import { createMemo, createUniqueId } from 'solid-js'import { Portal } from 'solid-js/web'
export default function ColorPicker() { const [state, send] = useMachine(colorPicker.machine({ id: createUniqueId(), value: colorPicker.parse('hsl(240,5.9%,10%)'), }))
const api = createMemo(() => colorPicker.connect(state, send, normalizeProps))
return ( <div {...api().getRootProps()}> <input {...api().getHiddenInputProps()} /> <div {...api().getControlProps()} > <button {...api().getTriggerProps()}> <div {...api().getTransparencyGridProps({ size: '8px' })} /> <div {...api().getSwatchProps({ value: api().value })} /> </button> <input {...api().getChannelInputProps({ channel: 'hex' })}/> <input {...api().getChannelInputProps({ channel: 'alpha' })}/> </div>
{api().open && ( <Portal mount={document.body}> <div {...api().getPositionerProps()} > <div {...api().getContentProps()} > <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: '8px' })} /> <div {...api().getChannelSliderTrackProps({ channel: 'alpha' })} /> <div {...api().getChannelSliderThumbProps({ channel: 'alpha' })} /> </div> </div> </div> </Portal>
)} </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 color -
onValueChangeEnd
— 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 } }, }),)
Using a custom color format
By default, the color picker's output format is rgba. You can change this format to either
hslaor
hsbaby 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 } }, }),)
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.setOpen(...)
method to control the
open and closed state of the color picker.
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( colorPicker.machine({ name: "color-preference", }),)
Styling Guide
Earlier, we mentioned that each collapse part has a
data-part
attribute added to them to select and style them in the DOM.
Open and closed state
When a color picker is expanded or collapsed, a data-state
attribute is set on the root,
trigger and content elements. This attribute is removed when it is closed.
[data-part="control"][data-state="open"] { /* styles for control open or state */}
[data-part="trigger"][data-state="open"] { /* styles for control open or state */}
[data-part="content"][data-state="open"] { /* styles for control open or state */}
Focused State
When the color picker 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 color picker 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:
Partial<{ root: string; control: string; trigger: string; label: string; input: string; hiddenInput: string; content: string; area: string; areaGradient: string; positioner: string; formatSelect: string; areaThumb: string; channelInput(id: string): string; channelSliderTrack(id: ColorChannel): string; }>
Color
boolean
boolean
boolean
boolean
(details: ValueChangeDetails) => void
(details: ValueChangeDetails) => void
(details: OpenChangeDetails) => void
string
PositioningOptions
() => HTMLElement
boolean
boolean
ColorFormat
(details: FormatChangeDetails) => void
boolean
boolean
string
() => Node | ShadowRoot | Document
"ltr" | "rtl"
(event: PointerDownOutsideEvent) => void
(event: FocusOutsideEvent) => void
(event: InteractOutsideEvent) => void
Machine API
The color picker api
exposes the following methods:
boolean
boolean
Color
string
(value: string | Color) => void
(channel: ColorChannel) => string
(channel: ColorChannel, locale: string) => string
(channel: ColorChannel, value: number) => void
ColorFormat
(format: ColorFormat) => void
number
(value: number) => void
(open: boolean) => void
Data Attributes
Root
data-scope
data-part
data-disabled
data-readonly
data-invalid
Label
data-scope
data-part
data-disabled
data-readonly
data-invalid
data-focus
Control
data-scope
data-part
data-disabled
data-readonly
data-invalid
data-state
data-focus
Trigger
data-scope
data-part
data-disabled
data-readonly
data-invalid
data-placement
data-state
data-focus
Content
data-scope
data-part
data-placement
data-state
ValueText
data-scope
data-part
data-disabled
data-focus
Area
data-scope
data-part
data-invalid
data-disabled
data-readonly
AreaBackground
data-scope
data-part
data-invalid
data-disabled
data-readonly
AreaThumb
data-scope
data-part
data-disabled
data-invalid
data-readonly
ChannelSlider
data-scope
data-part
data-channel
data-orientation
ChannelSliderTrack
data-scope
data-part
data-channel
data-orientation
ChannelSliderLabel
data-scope
data-part
data-channel
ChannelSliderValueText
data-scope
data-part
data-channel
ChannelSliderThumb
data-scope
data-part
data-channel
data-disabled
data-orientation
ChannelInput
data-scope
data-part
data-channel
data-disabled
data-invalid
data-readonly
EyeDropperTrigger
data-scope
data-part
data-disabled
data-invalid
data-readonly
SwatchTrigger
data-scope
data-part
data-state
data-value
data-disabled
Swatch
data-scope
data-part
data-state
data-value
Accessibility
Keyboard Interaction
Enter
ArrowLeft
ArrowRight
ArrowUp
ArrowDown
Esc