Color Picker
Accessible, unstyled color picker: 2D area, channel sliders, swatches, text field, and eye dropper.
| Package | @vizejs/ui/color-picker |
| Maturity | stable |
| Own the source | vize lib pull color-picker |
| Requires | collection, composite-navigation, context, controllable-state, id |
| Aliases | color picker, colour picker, color input, hue slider, eye dropper |
| Covers | HTML input type=color, React Aria ColorPicker, ColorArea, ColorSlider, ColorSwatchPicker, ColorField, Ark UI ColorPicker, EyeDropper API, CSS Color Module Level 4 rgb()/hsl() syntax |
Minimal setup
Use a Vue 3.5+ project with Vue SFC compilation configured (see Vite+ integration).
vp install @vizejs/ui
Add the public import below to <script setup lang="ts">. Components provide behavior without styles; the preview uses optional Paper styles.
import { ColorPicker, ColorPickerRoot, ColorPickerArea, ColorPickerChannelSlider, ColorPickerEyeDropper, ColorPickerField, ColorPickerSwatch, ColorPickerSwatchGroup } from "@vizejs/ui/color-picker";
Or copy the source into your project with vize lib pull color-picker (see Source Distribution).
Try it
This is the basic example maintained with the component source, using the public package imports. The preview adds the optional Paper styles.
Open the interactive example · Styling these components
Copy the example
<!-- Accent color picker with a saturation area, hue and alpha sliders, presets, and a hex field. -->
<script setup lang="ts">
import { ref } from "vue";
import {
ColorPicker,
ColorPickerArea,
ColorPickerChannelSlider,
ColorPickerField,
ColorPickerSwatch,
ColorPickerSwatchGroup,
} from "@vizejs/ui/color-picker";
const accent = ref("#3366cc");
// The 2D area needs a real size so pointer positions map onto saturation and brightness.
const areaSize = { inlineSize: "200px", blockSize: "140px" };
</script>
<template>
<div>
<ColorPicker v-model="accent" name="accent" aria-label="Accent color">
<ColorPickerArea :style="areaSize" />
<ColorPickerChannelSlider channel="hue" />
<ColorPickerChannelSlider channel="alpha" />
<ColorPickerSwatchGroup aria-label="Brand presets">
<ColorPickerSwatch value="#3366cc" label="Ocean" />
<ColorPickerSwatch value="#e4572e" label="Ember" />
<ColorPickerSwatch value="#2e933c" label="Fern" />
</ColorPickerSwatchGroup>
<ColorPickerField aria-label="Hex value" />
</ColorPicker>
<output>{{ accent }}</output>
</div>
</template>
API
ColorPicker
Source: src/families/form/color-picker/color-picker-root.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
id |
string | null |
undefined |
Consumer-owned base id. null and undefined select a deterministic fallback. |
modelValue |
string |
undefined |
Controlled color as any CSS color string accepted by parseColor. undefined selects uncontrolled behavior. Unparseable strings keep the last valid color. |
defaultValue |
string |
"#000000" |
Initial uncontrolled color, also restored by reset() and native form reset. |
format |
ColorFormat |
"hex" |
Serialization used for update:modelValue, slot value, and the form value. |
disabled |
boolean |
false |
Remove every part from interaction and sequential focus. |
readOnly |
boolean |
false |
Keep parts focusable while rejecting edits. |
required |
boolean |
false |
Mark the form value as required (forwarded to the hidden input's data-required). |
name |
string |
undefined |
Native form field name. When set, a hidden input submits the formatted value. |
form |
string |
undefined |
Id of a form owner outside the root's DOM ancestry. |
dir |
ColorPickerDirection |
"ltr" |
Reading direction used for horizontal pointer and arrow-key mapping. |
messages |
ColorPickerMessages |
undefined |
Localized accessible strings (channel names, value text, area name). Omitted entries use English defaults. |
Events
| Event | Payload | Description |
|---|---|---|
update:modelValue |
[value: string] |
Fired with the formatted value whenever the color changes. |
change |
[value: string, detail: ColorPickerChangeDetail] |
Fired after every distinct color change with the normalized color and its source. |
commit |
[value: string, color: ColorValue] |
Fired when a continuous interaction (drag end, key press, field commit, pick) settles. |
Slots
| Slot | Slot props | Description |
|---|---|---|
default |
ColorPickerSlotState |
Compound ColorPicker parts. Receives the current color and availability state. |
Exposed
| Member | Type | Description |
|---|---|---|
color |
— | |
disabled |
— | |
element |
Readonly<ShallowRef<HTMLElement | null>> |
Template ref to the rendered root element. |
format |
— | |
id |
— | |
readOnly |
— | |
reset |
— | |
setColor |
— | |
state |
— | |
value |
— |
ColorPickerRoot
Source: src/families/form/color-picker/color-picker-root.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
id |
string | null |
undefined |
Consumer-owned base id. null and undefined select a deterministic fallback. |
modelValue |
string |
undefined |
Controlled color as any CSS color string accepted by parseColor. undefined selects uncontrolled behavior. Unparseable strings keep the last valid color. |
defaultValue |
string |
"#000000" |
Initial uncontrolled color, also restored by reset() and native form reset. |
format |
ColorFormat |
"hex" |
Serialization used for update:modelValue, slot value, and the form value. |
disabled |
boolean |
false |
Remove every part from interaction and sequential focus. |
readOnly |
boolean |
false |
Keep parts focusable while rejecting edits. |
required |
boolean |
false |
Mark the form value as required (forwarded to the hidden input's data-required). |
name |
string |
undefined |
Native form field name. When set, a hidden input submits the formatted value. |
form |
string |
undefined |
Id of a form owner outside the root's DOM ancestry. |
dir |
ColorPickerDirection |
"ltr" |
Reading direction used for horizontal pointer and arrow-key mapping. |
messages |
ColorPickerMessages |
undefined |
Localized accessible strings (channel names, value text, area name). Omitted entries use English defaults. |
Events
| Event | Payload | Description |
|---|---|---|
update:modelValue |
[value: string] |
Fired with the formatted value whenever the color changes. |
change |
[value: string, detail: ColorPickerChangeDetail] |
Fired after every distinct color change with the normalized color and its source. |
commit |
[value: string, color: ColorValue] |
Fired when a continuous interaction (drag end, key press, field commit, pick) settles. |
Slots
| Slot | Slot props | Description |
|---|---|---|
default |
ColorPickerSlotState |
Compound ColorPicker parts. Receives the current color and availability state. |
Exposed
| Member | Type | Description |
|---|---|---|
value |
string |
Current value serialized in the root format. |
color |
ColorValue |
Current normalized color. |
format |
ColorFormat |
Serialization used for value, the hidden form input, and update:modelValue. |
disabled |
boolean |
Whether every part suppresses interaction and focus. |
readOnly |
boolean |
Whether parts stay focusable but reject edits. |
state |
ColorPickerState |
Stable state token for styling and tests. |
element |
HTMLDivElement | null |
Rendered root element. |
id |
string |
Root-owned deterministic base id. |
setColor |
(color: ColorValue | string) => boolean |
Request a color (object or CSS string) and report whether it changed. Invalid strings are ignored. |
reset |
() => boolean |
Restore the default value and report whether it changed. |
ColorPickerArea
Source: src/families/form/color-picker/color-picker-area.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
xChannel |
ColorChannel |
"saturation" |
Channel mapped to the horizontal axis (inline start = minimum). |
yChannel |
ColorChannel |
"brightness" |
Channel mapped to the vertical axis (bottom = minimum). |
space |
ColorSpace |
"hsb" |
Color space used to resolve hue, saturation, and alpha channels. |
ariaLabel |
string |
undefined |
Accessible name of the 2D slider thumb. Defaults to "<X> and <Y>". |
ariaLabelledby |
string |
undefined |
Space-separated ids that label the thumb. |
ariaDescribedby |
string |
undefined |
Space-separated ids that describe the thumb. |
Slots
| Slot | Slot props | Description |
|---|---|---|
default |
ColorPickerAreaSlotState |
Thumb contents. Receives both channel values and thumb position. |
Exposed
| Member | Type | Description |
|---|---|---|
element |
HTMLDivElement | null |
Rendered area element. |
thumb |
HTMLDivElement | null |
Rendered focusable thumb element. |
focus |
(options?: FocusOptions) => void |
Move focus to the thumb. |
ColorPickerChannelSlider
Source: src/families/form/color-picker/color-picker-channel-slider.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
channel (required) |
ColorChannel |
required |
Channel edited by this slider, e.g. "hue" or "alpha". |
space |
ColorSpace |
"hsb" |
Color space used to resolve hue, saturation, and alpha. |
orientation |
ColorPickerOrientation |
"horizontal" |
Layout axis. Vertical sliders place the minimum at the bottom. |
step |
number |
undefined |
Arrow-key and snapping increment. Defaults to the channel's natural step. |
ariaLabel |
string |
undefined |
Accessible name. Defaults to the English channel label, e.g. "Hue". |
ariaLabelledby |
string |
undefined |
Space-separated ids that label the thumb. |
ariaDescribedby |
string |
undefined |
Space-separated ids that describe the thumb. |
Slots
| Slot | Slot props | Description |
|---|---|---|
default |
ColorPickerChannelSliderSlotState |
Thumb contents. Receives the channel value and thumb position. |
Exposed
| Member | Type | Description |
|---|---|---|
element |
HTMLDivElement | null |
Rendered track element. |
thumb |
HTMLDivElement | null |
Rendered focusable thumb element. |
value |
number |
Current snapped channel value. |
focus |
(options?: FocusOptions) => void |
Move focus to the thumb. |
ColorPickerEyeDropper
Source: src/families/form/color-picker/color-picker-eye-dropper.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
unsupported |
ColorPickerEyeDropperFallback |
"disable" |
Rendering when the EyeDropper API is unavailable (and always during SSR): "disable" keeps a disabled button, "hide" adds the hidden attribute. |
preserveAlpha |
boolean |
true |
Keep the current alpha; the system picker always returns an opaque color. |
ariaLabel |
string |
undefined |
Accessible name when the default slot has no text. |
Events
| Event | Payload | Description |
|---|---|---|
pick |
[color: ColorValue, sRGBHex: string] |
Fired with the picked color after it has been applied. |
cancel |
[] |
Fired when the user dismisses the system picker. |
error |
[error: unknown] |
Fired when the picker rejects for a reason other than dismissal. |
Slots
| Slot | Slot props | Description |
|---|---|---|
default |
ColorPickerEyeDropperSlotState |
Button contents. Receives support and picking state. |
Exposed
| Member | Type | Description |
|---|---|---|
supported |
boolean |
Whether the browser provides the EyeDropper API (always false during SSR). |
picking |
boolean |
Whether the system picker is open. |
state |
ColorPickerEyeDropperState |
Stable state token for styling and tests. |
element |
HTMLButtonElement | null |
Rendered native button. |
open |
() => Promise<ColorValue | null> |
Open the system picker. Resolves with the picked color, or null when cancelled or unsupported. |
ColorPickerField
Source: src/families/form/color-picker/color-picker-field.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
format |
ColorFormat |
undefined |
Serialization shown in the field. Defaults to the root format. |
preserveAlpha |
boolean |
false |
Keep the current alpha when the typed color has no alpha component (for example typing #ff0000 into a half-transparent picker). |
ariaLabel |
string |
undefined |
Accessible name when no <label> or aria-labelledby supplies one. |
ariaLabelledby |
string |
undefined |
Space-separated ids that label the field. |
ariaDescribedby |
string |
undefined |
Space-separated ids that describe the field. |
placeholder |
string |
undefined |
Native placeholder text. |
Events
| Event | Payload | Description |
|---|---|---|
invalid |
[draft: string, nativeEvent: Event | null] |
Fired when a committed draft cannot be parsed and the field reverts. |
Exposed
| Member | Type | Description |
|---|---|---|
element |
HTMLInputElement | null |
Rendered native text input. |
draft |
string |
Current draft text. |
state |
ColorPickerFieldState |
Validity of the draft text. |
commit |
() => boolean |
Parse and commit the draft; invalid drafts revert. Returns whether it parsed. |
revert |
() => void |
Discard the draft and show the current color. |
focus |
(options?: FocusOptions) => void |
Move focus to the input. |
ColorPickerSwatch
Source: src/families/form/color-picker/color-picker-swatch.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
value (required) |
string |
required |
CSS color selected by this swatch, e.g. "#ff0000" or "hsl(210 80% 40%)". |
disabled |
boolean |
false |
Disable this swatch while keeping it visible. |
label |
string |
undefined |
Accessible name. Defaults to the color serialized in the root format. |
order |
number |
undefined |
Deterministic order for virtualized or portalled swatches. |
Events
| Event | Payload | Description |
|---|---|---|
click |
[nativeEvent: MouseEvent] |
Fired before selection. Call preventDefault() to keep the current color. |
Slots
| Slot | Slot props | Description |
|---|---|---|
default |
ColorPickerSwatchSlotState |
Swatch contents, e.g. a check mark. Receives selection state. |
Exposed
| Member | Type | Description |
|---|---|---|
value |
string |
Swatch color string exactly as supplied. |
color |
ColorValue |
Parsed swatch color. |
checked |
boolean |
Whether the swatch matches the root color. |
disabled |
boolean |
Whether the swatch or root suppresses selection. |
state |
ColorPickerSwatchState |
Stable state token for styling and tests. |
element |
HTMLDivElement | null |
Rendered swatch element. |
focus |
(options?: FocusOptions) => void |
Move focus to the swatch. |
ColorPickerSwatchGroup
Source: src/families/form/color-picker/color-picker-swatch-group.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
loop |
boolean |
true |
Whether arrow-key navigation wraps at the first and last enabled swatch. |
ariaLabel |
string |
undefined |
Accessible name of the radiogroup, e.g. "Saved colors". |
ariaLabelledby |
string |
undefined |
Space-separated ids that label the radiogroup. |
ariaDescribedby |
string |
undefined |
Space-separated ids that describe the radiogroup. |
Slots
| Slot | Slot props | Description |
|---|---|---|
default |
ColorPickerSlotState |
ColorPickerSwatch children. Receives the root color state. |
Exposed
| Member | Type | Description |
|---|---|---|
element |
HTMLDivElement | null |
Rendered radiogroup element. |
focus |
(options?: FocusOptions) => void |
Move focus to the checked, active, or first enabled swatch. |
Behavior
Normative state x input -> outcome table for color-picker-root.vue,
color-picker-area.vue, color-picker-channel-slider.vue,
color-picker-swatch-group.vue, color-picker-swatch.vue,
color-picker-field.vue, and color-picker-eye-dropper.vue
(@vizejs/ui/color-picker), plus the pure color model in
color-picker-color.ts. Every row is proven by the named test.
Value model
v-model is a CSS color string. Any syntax accepted by parseColor may be
supplied (#rgb[a], #rrggbb[aa], rgb()/rgba() and hsl()/hsla() in
legacy comma or CSS Color 4 space/slash syntax, hue units deg/grad/rad/
turn, the non-CSS hsb(), and transparent); emitted values are serialized
in the root format (hex by default, which becomes #rrggbbaa below full
opacity). Internally the root keeps the last accepted color as an immutable
HSB ColorValue, so hue survives greys and saturation survives black while a
user drags. The typed ColorValue is available through change details, slot
state, and setColor. Unparseable model strings keep the last valid color.
Table
| ID | State | Input | Outcome | Evidence |
|---|---|---|---|---|
| CP1 | model | parse hex | 3/4/6/8-digit hex parses case-insensitively; malformed hex returns null |
parses every hex notation and normalizes to lowercase |
| CP2 | model | parse rgb | legacy and modern rgb, percentages, and alpha parse; channels clamp like CSS; malformed argument lists return null |
parses legacy and modern rgb syntax with percentages and alpha |
| CP3 | model | parse hsl / hsb | hue units deg/grad/rad/turn and negative hues wrap; hsb notation parses; bad units return null |
parses hsl with angle units and hsb notation |
| CP4 | model | parse keyword / empty | transparent parses; empty and unsupported named colors return null |
parses transparent and rejects empty or unknown input |
| CP5 | model | format | every ColorFormat serializes with and without alpha; hex is the default |
formats every color format with and without alpha |
| CP6 | model | round-trip | 8-bit colors round-trip losslessly through hex, hex8, rgb, and hsb | round-trips every 8-bit primary and secondary through each format |
| CP7 | model | convert | hsb ↔ rgb ↔ hsl conversions are exact | converts between hsb, rgb, and hsl exactly |
| CP8 | model | achromatic convert | greys keep the fallback hue; black keeps the fallback saturation | achromatic conversions keep the fallback hue and saturation |
| CP9 | model | create | hue wraps, channels clamp, non-finite channels fall back, results are frozen | createColor wraps hue and clamps every channel |
| CP10 | model | channel get/set | every channel reads and writes in its space; out-of-range values clamp | reads and writes every channel in its color space |
| CP11 | model | channel metadata | space resolution, ranges, labels, snapping, and aria-valuetext strings are stable |
resolves channel spaces, ranges, labels, snapping, and value text |
| CP12 | model | gradient | channel track gradients interpolate exactly (hue uses six segments, lightness three stops) | builds exact channel preview gradients |
| CP13 | model | equality / guards | equality is 8-bit sRGB plus 1/255 alpha; isColorValue / isColorFormat narrow unknown input |
compares colors at 8-bit precision and guards unknown values |
| CP14 | helpers | pointer / keys / platform | pointer fractions clamp and flip in RTL; key intents cover arrows, Shift, Page keys, Home/End; capture and EyeDropper helpers tolerate missing APIs; swatch ids are DOM-safe | maps pointer coordinates to clamped fractions in both directions, maps fractions and values across channel ranges, resolves one- and two-dimensional keyboard intents, pointer capture helpers tolerate missing or throwing platform APIs, eye dropper helpers feature-detect and validate untyped platform values, creates DOM-id-safe swatch segments for arbitrary color strings |
| CP15 | root default | render | role="group" root exposes id, data-state/value, --vize-ui-color-picker-* custom properties, slot state, and a hidden form input |
renders a labelled root with color custom properties, slot state, and form value |
| CP16 | area | pointer down / move / up | pointer position maps to saturation (x) and brightness (y, bottom = 0), focuses the thumb, publishes thumb vars, commits on release | area maps pointer drags to saturation and brightness with a committed release |
| CP17 | area | Arrow / Shift+Arrow / Page / Home / End | one role="slider" thumb with aria-roledescription="2D slider" and combined aria-valuetext; each key edits one axis and commits |
area thumb exposes 2D slider semantics and keyboard steps |
| CP18 | area, custom axes | keys through grey | hue survives achromatic intermediates; any channel pair (e.g. hue × lightness) can drive the area | area keeps the hue while dragging through greys and supports custom channels |
| CP19 | channel slider | keys / pointer | hue, alpha (vertical), and rgb channels step, page, jump, and follow pointer position; track gradient and percent vars update | channel sliders edit hue, alpha, and rgb channels with keyboard and pointer |
| CP20 | dir="rtl" |
ArrowRight / pointer | horizontal arrows and pointer mapping invert | right-to-left layouts invert horizontal arrows and pointer mapping |
| CP21 | swatch group | Tab / Arrow / click / Space | radiogroup of radio swatches with roving tabindex; arrows move and select skipping disabled swatches; click and Space select |
swatches form a roving radiogroup that selects on click and arrow keys |
| CP22 | swatch | click with preventDefault() |
selection is cancelled | swatch click can be cancelled before selection |
| CP23 | field | type / Enter / blur / Escape / commit() | drafts validate live (aria-invalid), Enter and blur commit parseable text, invalid commits emit invalid and revert, Escape reverts |
field parses on Enter and blur, flags invalid drafts, and reverts them |
| CP24 | field preserveAlpha |
commit opaque text | the current alpha is kept and the field can use its own format | field can preserve the current alpha and follow its own format |
| CP25 | controlled | edit | emits the requested value while rendering the controlled color until the parent accepts it; invalid model keeps the last color | controlled value wins until the parent accepts the request |
| CP26 | disabled / read-only | keys / pointer / Tab | disabled removes thumbs from the tab order and disables inputs; read-only stays focusable, marks aria-readonly, rejects edits |
disabled and read-only roots suppress edits while read-only stays focusable |
| CP27 | exposed root | setColor / reset | exposes value, color, format, state, element; setColor accepts strings or ColorValue and rejects invalid strings |
exposes typed state and imperative setColor/reset controls |
| CP28 | inside <form> |
native reset | uncontrolled roots restore defaultValue and the submitted value |
native form reset restores the default value |
| CP29 | eye dropper supported | click / open() | opens the system picker, applies the sRGB result (keeping alpha by default), emits pick; AbortError emits cancel, others error |
eye dropper feature-detects, picks, preserves alpha, and reports cancel |
| CP30 | eye dropper unsupported | render | data-state="unsupported"; unsupported="disable" disables, "hide" adds hidden |
eye dropper disables or hides itself without platform support |
| CP31 | missing provider | setup | parts fail closed with the shared context diagnostic | compound parts require a matching root provider |
| CP32 | SSR and hydration | render twice / hydrate | markup is byte-identical per request (eye dropper renders unsupported on the server) and hydrates without warnings or id changes | renders byte-identical color picker markup across isolated SSR requests, hydrates generated color picker ids without changing the server contract |
| CP33 | messages |
render | channel names, aria-valuetext, and the 2D area name come from the typed messages prop; omitted entries use English defaults |
messages localize channel names, value text, and the area name |
Notes
- Area keyboard map: Left/Right edit the x channel, Up/Down the y channel,
Shift multiplies to the channel page step, Page Up/Down page the y channel,
Home/End jump the x channel. Horizontal keys invert under
dir="rtl". - Default accessible names are English (
"Hue","Saturation and Brightness"); passariaLabel/ariaLabelledbyto localize. commitfires once per settled interaction (pointer release, each key press, field commit, swatch selection, eye-dropper pick);changefires for every intermediate color.- No CSS ships. Style from
data-*hooks and the published custom properties:--vize-ui-color-picker-color,-opaque-color,-hue,-alpha(root),-thumb-x,-thumb-y,-area-hue,-area-background(area),-thumb-percent,-track-background(slider),-swatch-color(swatch).