Choose a color by dragging across an area and along sliders, typing any CSS color, or picking a preset.

Use a color picker to choose a color, like a brand or theme color. The value is a CSS color string, and every part edits it: an area for two channels at once, a slider per channel, a text input that reads any CSS color, and presets.

<script lang="ts">
  import { Pipette } from "@lucide/svelte"
  import {
    formatColor,
    parseColor,
    type ColorFormat,
  } from "@ui-sdk/svelte/color"
  import {
    ColorPicker,
    ColorPickerArea,
    ColorPickerAreaThumb,
    ColorPickerEyeDropper,
    ColorPickerInput,
    ColorPickerSlider,
    ColorPickerSliderThumb,
    ColorPickerSwatch,
    ColorPickerSwatches,
    ColorPickerSwatchItem,
  } from "@ui-sdk/svelte/color-picker"

  const formats: { label: string; value: ColorFormat }[] = [
    { label: "Hex", value: "hex" },
    { label: "RGB", value: "srgb" },
    { label: "HSL", value: "hsl" },
    { label: "OKLCH", value: "oklch" },
  ]

  const presets = [
    "#ef4444",
    "#f97316",
    "#eab308",
    "#22c55e",
    "#0ea5e9",
    "#6366f1",
    "#a855f7",
    "#ec4899",
  ]

  let format = $state<ColorFormat>("hex")
  let value = $state("#0ea5e9")

  function changeFormat(next: ColorFormat) {
    format = next
    value = formatColor(parseColor(value)!, next)
  }
</script>

<ColorPicker bind:value aria-label="Brand color" {format}>
  <ColorPickerArea>
    <ColorPickerAreaThumb />
  </ColorPickerArea>
  <div data-slot="row">
    <ColorPickerEyeDropper aria-label="Pick from screen">
      <Pipette aria-hidden="true" />
    </ColorPickerEyeDropper>
    <div data-slot="color-picker-sliders">
      <ColorPickerSlider channel="hue">
        <ColorPickerSliderThumb />
      </ColorPickerSlider>
      <ColorPickerSlider channel="alpha">
        <ColorPickerSliderThumb />
      </ColorPickerSlider>
    </div>
    <ColorPickerSwatch />
  </div>
  <div data-slot="row">
    <select
      aria-label="Format"
      value={format}
      onchange={(event) =>
        changeFormat(event.currentTarget.value as ColorFormat)}
    >
      {#each formats as option (option.value)}
        <option value={option.value}>{option.label}</option>
      {/each}
    </select>
    <ColorPickerInput aria-label="Color" />
  </div>
  <ColorPickerSwatches aria-label="Presets">
    {#each presets as preset (preset)}
      <ColorPickerSwatchItem value={preset} />
    {/each}
  </ColorPickerSwatches>
</ColorPicker>
Drag across the area or along the sliders, or choose a preset. Choose HSL to write the color another way, or type one like rebeccapurple and press Enter.

Installation

npm install @ui-sdk/svelte @ui-sdk/core

Composition

Use the following composition to build a ColorPicker:

ColorPicker
โ”œโ”€โ”€ ColorPickerArea
โ”‚   โ”œโ”€โ”€ ColorPickerAreaCanvas
โ”‚   โ””โ”€โ”€ ColorPickerAreaThumb
โ”œโ”€โ”€ ColorPickerSlider
โ”‚   โ””โ”€โ”€ ColorPickerSliderThumb
โ”œโ”€โ”€ ColorPickerSlider
โ”‚   โ””โ”€โ”€ ColorPickerSliderThumb
โ”œโ”€โ”€ ColorPickerSwatch
โ”œโ”€โ”€ ColorPickerInput
โ”œโ”€โ”€ ColorPickerChannelInput
โ”œโ”€โ”€ ColorPickerEyeDropper
โ””โ”€โ”€ ColorPickerSwatches
    โ”œโ”€โ”€ ColorPickerSwatchItem
    โ””โ”€โ”€ ColorPickerSwatchItem

Every part is optional, so use the ones your picker needs.

  • ColorPicker holds the value and is a group around the parts.
  • ColorPickerArea edits two channels, like saturation across and brightness up. A press moves the thumb there and starts a drag. Its background is --color-area-background, so give it a size and that background.
  • ColorPickerAreaCanvas paints the area pixel by pixel, for channels no gradient can draw, like OKLCH's chroma and lightness.
  • ColorPickerAreaThumb shows where the color sits in the area. It's a slider that screen readers hear as both channels.
  • ColorPickerSlider edits one channel, like hue or alpha, drawn by --color-slider-background.
  • ColorPickerSliderThumb shows where the channel sits along its slider.
  • ColorPickerSwatch shows the color, named for screen readers, like "vivid blue".
  • ColorPickerInput reads any CSS color when you press Enter or leave it. Text that isn't a color goes back to the value.
  • ColorPickerChannelInput edits one channel as a number, in the units people read it in, like 0 to 255 for red.
  • ColorPickerEyeDropper picks a color from anywhere on the screen, where the browser can.
  • ColorPickerSwatches and ColorPickerSwatchItem are a list of preset colors.

Each part writes the color as --color-picker-color, so a thumb or a swatch fills itself with background: var(--color-picker-color). The picker also renders a hidden input that the form submits.

Usage

Import the parts and put them together:

<script lang="ts">
  import {
    ColorPicker,
    ColorPickerArea,
    ColorPickerAreaThumb,
    ColorPickerInput,
    ColorPickerSlider,
    ColorPickerSliderThumb,
  } from "@ui-sdk/svelte/color-picker"
</script>

<ColorPicker aria-label="Brand color" defaultValue="#0ea5e9">
  <ColorPickerArea>
    <ColorPickerAreaThumb />
  </ColorPickerArea>
  <ColorPickerSlider channel="hue">
    <ColorPickerSliderThumb />
  </ColorPickerSlider>
  <ColorPickerInput aria-label="Hex" />
</ColorPicker>

Name the picker with aria-label, or put it in a Field whose label names it. Without defaultValue, it starts at black.

Spaces and Channels

space decides which channels the parts edit, and what the value is written in:

spaceArea, across and upSliderValue written as
hsvsaturation, brightnesshuehex, like #0ea5e9
hslsaturation, lightnesshuehsl(198.63 88.66% 48.43%)
srgbred, greenbluergb(14 165 233)
oklchchroma, lightnesshueoklch(0.6847 0.1479 237.32)

Set format to write the value another way, like format="hex" in any space. Every space has an alpha channel too. Set x and y on the area, or channel on a slider, to edit other channels:

<ColorPicker
  aria-label="Background"
  space="hsl"
  defaultValue="hsl(220 60% 50%)"
>
  <ColorPickerArea x="hue" y="lightness">
    <ColorPickerAreaCanvas />
    <ColorPickerAreaThumb />
  </ColorPickerArea>
  <ColorPickerSlider channel="saturation" orientation="vertical">
    <ColorPickerSliderThumb />
  </ColorPickerSlider>
</ColorPicker>

The parts keep a hue while the color turns gray or black, so dragging back out of a corner returns to the same hue.

OKLCH

OKLCH spaces its colors evenly for the eye, so two colors with the same lightness look equally bright. Its edge with the colors a screen can show is a curve, which no gradient can draw. Add ColorPickerAreaCanvas to the area, and it paints the colors inside that edge and leaves the rest clear.

<script lang="ts">
  import {
    ColorPicker,
    ColorPickerArea,
    ColorPickerAreaCanvas,
    ColorPickerAreaThumb,
    ColorPickerInput,
    ColorPickerSlider,
    ColorPickerSliderThumb,
    ColorPickerSwatch,
  } from "@ui-sdk/svelte/color-picker"
</script>

<ColorPicker
  aria-label="Accent"
  space="oklch"
  defaultValue="oklch(0.62 0.19 259)"
>
  <ColorPickerArea>
    <ColorPickerAreaCanvas />
    <ColorPickerAreaThumb />
  </ColorPickerArea>
  <ColorPickerSlider channel="hue">
    <ColorPickerSliderThumb />
  </ColorPickerSlider>
  <ColorPickerSlider channel="alpha">
    <ColorPickerSliderThumb />
  </ColorPickerSlider>
  <div data-slot="row">
    <ColorPickerSwatch />
    <ColorPickerInput aria-label="OKLCH" />
  </div>
</ColorPicker>
Drag the thumb right, past the painted edge, and let go. The thumb moves back to the closest color inside.

While a drag holds a color past the edge, the value is already the closest color inside it, and every part has data-out-of-gamut. Once the drag ends, the thumb moves to that color. Colors stay inside sRGB by default. Set gamut="p3" for wide-gamut screens, or gamut={false} to allow any color:

<ColorPicker
  aria-label="Accent"
  space="oklch"
  gamut="p3"
  defaultValue="oklch(0.7 0.25 150)"
>
  <!-- โ€ฆ -->
</ColorPicker>

Presets and Channel Inputs

Put ColorPickerSwatchItems inside ColorPickerSwatches for preset colors. Each one is selected while the value is its color, however either is written, so #0000ff selects rgb(0 0 255). The list is one tab stop, and the arrow keys move between the presets.

ColorPickerChannelInput edits one channel as a number and sets it on Enter, when you leave it, or with its arrows. ColorPickerEyeDropper picks from the screen in browsers that can. Elsewhere it's disabled, with data-unsupported, so you can hide it.

<script lang="ts">
  import { Pipette } from "@lucide/svelte"
  import {
    ColorPicker,
    ColorPickerChannelInput,
    ColorPickerEyeDropper,
    ColorPickerSwatches,
    ColorPickerSwatchItem,
  } from "@ui-sdk/svelte/color-picker"

  const presets = [
    "#ef4444",
    "#f97316",
    "#eab308",
    "#22c55e",
    "#0ea5e9",
    "#6366f1",
    "#a855f7",
    "#ec4899",
  ]
</script>

<ColorPicker
  aria-label="Label color"
  space="srgb"
  format="hex"
  defaultValue="#0ea5e9"
>
  <ColorPickerSwatches aria-label="Presets">
    {#each presets as preset (preset)}
      <ColorPickerSwatchItem value={preset} />
    {/each}
  </ColorPickerSwatches>
  <div data-slot="row">
    <ColorPickerChannelInput channel="red" />
    <ColorPickerChannelInput channel="green" />
    <ColorPickerChannelInput channel="blue" />
    <ColorPickerEyeDropper aria-label="Pick from screen">
      <Pipette aria-hidden="true" />
    </ColorPickerEyeDropper>
  </div>
</ColorPicker>
Choose a preset, or use the arrow keys between them. Type 255 into the first number to set red.

Popover

Put a Popover inside the picker to open it from a swatch. The parts inside the popup edit the same color, and the swatch in the trigger shows it.

<script lang="ts">
  import {
    ColorPicker,
    ColorPickerArea,
    ColorPickerAreaThumb,
    ColorPickerInput,
    ColorPickerSlider,
    ColorPickerSliderThumb,
    ColorPickerSwatch,
  } from "@ui-sdk/svelte/color-picker"
  import {
    Popover,
    PopoverContent,
    PopoverTrigger,
  } from "@ui-sdk/svelte/popover"
</script>

<ColorPicker aria-label="Background" defaultValue="#22c55e">
  <Popover>
    <PopoverTrigger aria-label="Background">
      <ColorPickerSwatch />
    </PopoverTrigger>
    <PopoverContent>
      <ColorPickerArea>
        <ColorPickerAreaThumb />
      </ColorPickerArea>
      <ColorPickerSlider channel="hue">
        <ColorPickerSliderThumb />
      </ColorPickerSlider>
      <ColorPickerInput aria-label="Hex" />
    </PopoverContent>
  </Popover>
</ColorPicker>
Press the Background swatch to open the picker, then drag to change the color.

Disabled

Set disabled. A disabled picker can't be focused or changed, and isn't submitted. A disabled <fieldset> around it disables it too.

<script lang="ts">
  import {
    ColorPicker,
    ColorPickerArea,
    ColorPickerAreaThumb,
    ColorPickerInput,
    ColorPickerSlider,
    ColorPickerSliderThumb,
  } from "@ui-sdk/svelte/color-picker"
</script>

<ColorPicker aria-label="Brand color" defaultValue="#0ea5e9" disabled>
  <ColorPickerArea>
    <ColorPickerAreaThumb />
  </ColorPickerArea>
  <ColorPickerSlider channel="hue">
    <ColorPickerSliderThumb />
  </ColorPickerSlider>
  <ColorPickerInput aria-label="Hex" />
</ColorPicker>
The thumbs can't be focused or dragged.

Set readOnly to keep the parts focusable and the value submitted, while drags, keys and typing leave the color alone:

<ColorPicker aria-label="Theme color" defaultValue="#0ea5e9" readOnly>
  <ColorPickerSwatch />
  <ColorPickerInput aria-label="Hex" />
</ColorPicker>

Controlled

Bind value with bind:value, and pass onValueChange for the details. Each change carries a reason, like drag, step-key or input-commit, and details.color, the color the parts hold, which keeps a hue while the value turns gray. Call details.cancel() in the callback to keep the current value.

onValueChange runs on every step of a drag. onValueCommit runs once, when the drag, key press or typed color ends, so use it to save. A controller from createColorPickerController() sets the color from anywhere with setValue(), like the Use Purple button below.

Console
<script lang="ts">
  import {
    ColorPicker,
    ColorPickerArea,
    ColorPickerAreaThumb,
    ColorPickerInput,
    ColorPickerSlider,
    ColorPickerSliderThumb,
    createColorPickerController,
  } from "@ui-sdk/svelte/color-picker"
  import { useLog } from "./log"

  const log = useLog()
  const picker = createColorPickerController()
  let value = $state("#0ea5e9")
</script>

<div>
  <div data-slot="controls">
    <button type="button" onclick={() => picker.setValue("#663399")}
      >Use Purple</button
    >
  </div>
  <ColorPicker
    aria-label="Theme color"
    bind:value
    onValueCommit={(next, details) =>
      log(`committed: ${next} (${details.reason})`)}
    controller={picker}
  >
    <ColorPickerArea>
      <ColorPickerAreaThumb />
    </ColorPickerArea>
    <ColorPickerSlider channel="hue">
      <ColorPickerSliderThumb />
    </ColorPickerSlider>
    <ColorPickerInput aria-label="Hex" />
  </ColorPicker>
</div>
Drag across the area and let go, or press Use Purple. The console logs each committed color and its reason.

See State and Events for controlled state, cancellation and debugging.

Form

The hidden input submits the value under name, like #f97316, and a disabled picker submits nothing. A form reset returns it to defaultValue. Set form to join a form by its id.

Console
<script lang="ts">
  import {
    ColorPicker,
    ColorPickerArea,
    ColorPickerAreaThumb,
    ColorPickerSlider,
    ColorPickerSliderThumb,
    ColorPickerSwatch,
  } from "@ui-sdk/svelte/color-picker"
  import { Field, FieldLabel } from "@ui-sdk/svelte/field"
  import { useLog } from "./log"

  const log = useLog()

  function onsubmit(event: SubmitEvent & { currentTarget: HTMLFormElement }) {
    event.preventDefault()
    log(`Submitted brand=${new FormData(event.currentTarget).get("brand")}`)
  }
</script>

<form data-slot="stack" {onsubmit}>
  <Field>
    <FieldLabel>Brand color</FieldLabel>
    <ColorPicker name="brand" defaultValue="#f97316">
      <ColorPickerArea>
        <ColorPickerAreaThumb />
      </ColorPickerArea>
      <div data-slot="row">
        <ColorPickerSwatch />
        <ColorPickerSlider channel="hue">
          <ColorPickerSliderThumb />
        </ColorPickerSlider>
      </div>
    </ColorPicker>
  </Field>
  <div data-slot="row">
    <button type="submit" data-variant="solid">Save</button>
    <button type="reset">Reset</button>
  </div>
</form>
Change the color and press Save to log the submitted value. Reset restores the orange.

A color picker always has a value, so required only adds data-required.

Custom Rendering

Every part takes render, a snippet that renders your own element with the part's props and state, and its children snippet gets the state. A slider thumb's state has its channelValue, a preset's has whether it's selected, and every part has the color and whether it's outOfGamut:

<ColorPickerSlider channel="alpha">
  <ColorPickerSliderThumb>
    {#snippet children(state)}
      <span class:dashed={state.outOfGamut}
        >{Math.round(state.channelValue)}%</span
      >
    {/snippet}
  </ColorPickerSliderThumb>
</ColorPickerSlider>
<ColorPickerSwatch>
  {#snippet children(state)}
    {#if state.color.alpha < 1}<span>Transparent</span>{/if}
  {/snippet}
</ColorPickerSwatch>

See Parts and Composition for the rendering contract and State for each part's fields.

Styling

Parts are unstyled, apart from the positions they need: thumbs get position: absolute and their place in percent, as inline styles. Give the area and sliders a size and their background variable, and center each thumb on its point with translate: -50% -50%.

Each part has a data-slot, such as color-picker-area, and its state attributes are listed under Data Attributes.

ColorPicker

VariableWhat it holds
--color-picker-colorThe color as CSS, inside the gamut. On a swatch item, its own color. Use it to fill a thumb or a swatch.

ColorPickerArea

VariableWhat it holds
--color-area-backgroundGradients that draw the area, when its channels are saturation and brightness or lightness. Unset for others, which a canvas paints.

ColorPickerAreaThumb

VariableWhat it holds
--area-xWhere the thumb is across the area, from 0 to 1.
--area-yWhere the thumb is up the area, from 0 at the bottom to 1 at the top.

ColorPickerSlider

VariableWhat it holds
--color-slider-backgroundA gradient through the slider's channel, with the other channels as they are.

ColorPickerSliderThumb

VariableWhat it holds
--color-slider-thumb-positionWhere the thumb is along the slider, as a percentage.

ColorPickerSwatch

VariableWhat it holds
--color-picker-colorThe color as CSS, inside the gamut. On a swatch item, its own color. Use it to fill a thumb or a swatch.

ColorPickerSwatchItem

VariableWhat it holds
--color-picker-colorThe color as CSS, inside the gamut. On a swatch item, its own color. Use it to fill a thumb or a swatch.

Data Attributes

ColorPicker

[data-slot="color-picker"]

AttributeWhat it means
data-disabledPresent when the picker is disabled, by its own prop or a <fieldset>.
data-readonlyPresent when the picker shows its color but can't change it.
data-requiredPresent when the picker is required.
data-out-of-gamutPresent while a drag or a key holds a color outside the gamut. The value is the closest color inside, and the parts move there once the change commits.

ColorPickerArea

[data-slot="color-picker-area"]

AttributeWhat it means
data-disabledPresent when the picker is disabled, by its own prop or a <fieldset>.
data-readonlyPresent when the picker shows its color but can't change it.
data-requiredPresent when the picker is required.
data-out-of-gamutPresent while a drag or a key holds a color outside the gamut. The value is the closest color inside, and the parts move there once the change commits.

ColorPickerAreaThumb

[data-slot="color-picker-area-thumb"]

AttributeWhat it means
data-area-thumbPresent on the area's thumb, which a press on the area moves and focuses.
data-disabledPresent when the picker is disabled, by its own prop or a <fieldset>.
data-readonlyPresent when the picker shows its color but can't change it.
data-requiredPresent when the picker is required.
data-out-of-gamutPresent while a drag or a key holds a color outside the gamut. The value is the closest color inside, and the parts move there once the change commits.

ColorPickerSlider

[data-slot="color-picker-slider"]

AttributeWhat it means
data-disabledPresent when the picker is disabled, by its own prop or a <fieldset>.
data-readonlyPresent when the picker shows its color but can't change it.
data-requiredPresent when the picker is required.
data-orientationhorizontalverticalThe slider's axis.
data-channelhuesaturationbrightnesslightnesschromaredgreenbluealphaThe channel the part edits.
data-out-of-gamutPresent while a drag or a key holds a color outside the gamut. The value is the closest color inside, and the parts move there once the change commits.

ColorPickerSliderThumb

[data-slot="color-picker-slider-thumb"]

AttributeWhat it means
data-disabledPresent when the picker is disabled, by its own prop or a <fieldset>.
data-readonlyPresent when the picker shows its color but can't change it.
data-requiredPresent when the picker is required.
data-orientationhorizontalverticalThe slider's axis.
data-channelhuesaturationbrightnesslightnesschromaredgreenbluealphaThe channel the part edits.
data-out-of-gamutPresent while a drag or a key holds a color outside the gamut. The value is the closest color inside, and the parts move there once the change commits.

ColorPickerInput

[data-slot="color-picker-input"]

AttributeWhat it means
data-disabledPresent when the picker is disabled, by its own prop or a <fieldset>.
data-readonlyPresent when the picker shows its color but can't change it.
data-requiredPresent when the picker is required.
data-out-of-gamutPresent while a drag or a key holds a color outside the gamut. The value is the closest color inside, and the parts move there once the change commits.

ColorPickerChannelInput

[data-slot="color-picker-channel-input"]

AttributeWhat it means
data-disabledPresent when the picker is disabled, by its own prop or a <fieldset>.
data-readonlyPresent when the picker shows its color but can't change it.
data-requiredPresent when the picker is required.
data-channelhuesaturationbrightnesslightnesschromaredgreenbluealphaThe channel the part edits.
data-out-of-gamutPresent while a drag or a key holds a color outside the gamut. The value is the closest color inside, and the parts move there once the change commits.

ColorPickerSwatch

[data-slot="color-picker-swatch"]

AttributeWhat it means
data-disabledPresent when the picker is disabled, by its own prop or a <fieldset>.
data-readonlyPresent when the picker shows its color but can't change it.
data-requiredPresent when the picker is required.
data-out-of-gamutPresent while a drag or a key holds a color outside the gamut. The value is the closest color inside, and the parts move there once the change commits.

ColorPickerEyeDropper

[data-slot="color-picker-eye-dropper"]

AttributeWhat it means
data-disabledPresent when the picker is disabled, by its own prop or a <fieldset>.
data-readonlyPresent when the picker shows its color but can't change it.
data-requiredPresent when the picker is required.
data-out-of-gamutPresent while a drag or a key holds a color outside the gamut. The value is the closest color inside, and the parts move there once the change commits.
data-unsupportedPresent when the browser can't pick a color from the screen.

ColorPickerSwatches

[data-slot="color-picker-swatches"]

AttributeWhat it means
data-disabledPresent when the picker is disabled, by its own prop or a <fieldset>.
data-readonlyPresent when the picker shows its color but can't change it.
data-requiredPresent when the picker is required.
data-out-of-gamutPresent while a drag or a key holds a color outside the gamut. The value is the closest color inside, and the parts move there once the change commits.

ColorPickerSwatchItem

[data-slot="color-picker-swatch-item"]

AttributeWhat it means
data-selectedPresent on the swatch item whose color is the value.
data-disabledPresent when the picker is disabled, by its own prop or a <fieldset>.
data-readonlyPresent when the picker shows its color but can't change it.

API Reference

Props and callbacks are the same as below, as Svelte props. value can be bound with bind:value.

ColorPicker

value?string
The value, as a CSS color, like "#0ea5e9" or "oklch(0.62 0.19 259)". Pass it to control the picker.
defaultValue?string
The value at first, when uncontrolled. A form reset returns to it.
onValueChange?((value: string, details: ColorPickerValueChangeDetails) => void)
Runs while the value changes: on every step of a drag or a key. details.color is the color, and details.cancel() stops it.
onValueCommit?((value: string, details: ColorPickerValueCommitDetails) => void)
Runs once an interaction that changed the value ends: the pointer lifts, a key is pressed, a swatch is picked. For saving.
space?ColorPickerSpace
The space the parts edit in, which decides their channels: "hsv" has hue, saturation and brightness, "hsl" hue, saturation and lightness, "srgb" red, green and blue, "oklch" lightness, chroma and hue.
format?ColorFormat
How the value is written: "hex", or a space in its CSS syntax, like "oklch".
gamut?false | ColorGamut
The colors the value can hold. A drag can go past the edge, and the value is the closest color inside it; once the drag ends, the picker moves there too. "p3" allows wide-gamut screens' colors, and false any color.
disabled?boolean
Disables every part: they can't be focused or changed, and the value isn't submitted.
readOnly?boolean
Keeps the parts focusable and the value submitted, but stops changes.
required?boolean
Marks it required, with data-required. A color picker always has a value, so it never blocks a form.
name?string
The name the value submits under. Without one, the picker submits nothing.
form?string
The id of a form to belong to, when it isn't inside that form.
controller?{ getValue: () => string; setValue: (value: string, options?: ColorPickerChangeOptions) => boolean; focus: () => void; }
Sets the value from anywhere. Create one with createColorPickerController().
debug?boolean
Logs every state change with its reason to the console, in development builds.

ColorPickerArea

x?ColorChannel
The channel across, left to right.
y?ColorChannel
The channel up, bottom to top.

ColorPickerSlider

channel?ColorChannel
The channel the slider edits, like "hue" or "alpha".
orientation?"horizontal" | "vertical"
The slider's axis. Vertical sliders grow upward.

ColorPickerChannelInput

channel?ColorChannel
The channel the input edits, like "hue" or "alpha".

ColorPickerSwatchItem

valuestring
The color it picks, as any CSS color.

State

What render functions, state-based class names and slots receive. Every part gets the picker's state.

valuestring
The value, as a CSS color.
colorColor
The color the parts show, in the picker's space. During a drag it can be outside the gamut.
spaceColorPickerSpace
The space the parts edit in.
disabledboolean
Whether it's disabled, by itself or by a <fieldset>.
readOnlyboolean
Whether it's read-only: the parts stay focusable, but nothing changes the value.
requiredboolean
Whether it's marked required.
outOfGamutboolean
Whether the color the parts show is outside the gamut, so the value is the closest one inside.

Sliders and their thumbs add their channel:

valuestring
The value, as a CSS color.
colorColor
The color the parts show, in the picker's space. During a drag it can be outside the gamut.
spaceColorPickerSpace
The space the parts edit in.
disabledboolean
Whether it's disabled, by itself or by a <fieldset>.
readOnlyboolean
Whether it's read-only: the parts stay focusable, but nothing changes the value.
requiredboolean
Whether it's marked required.
outOfGamutboolean
Whether the color the parts show is outside the gamut, so the value is the closest one inside.
channelColorChannel
The channel the slider edits.
channelValuenumber
The channel's value, in the units people read it in: degrees, percentages, 0 to 255 for red.
orientation"horizontal" | "vertical"
The slider's axis.

Preset items have their own:

selectedboolean
Whether the picker's color looks the same as this swatch's, whatever each is written in.
disabledboolean
Whether pressing it changes nothing, since the picker is disabled or read-only.

Reasons

details.reason says why the value changed.

track-pressPointerEvent
A press on a slider's track or an area's surface moved the nearest thumb there.
dragPointerEvent
A slider thumb or a resize handle was dragged, or an item was dragged to a new place in a list.
step-keyKeyboardEvent
Arrow, Page Up, Page Down, Home or End stepped a slider or a number input.
input-commitKeyboardEvent | FocusEvent
Enter, or leaving a number input, committed what was typed: a number input parses, clamps and snaps it to the step, and an autocomplete closes its suggestions on it.
item-pressMouseEvent | KeyboardEvent
A member of a collection was chosen: a menu item, a tab, a radio or a checkbox item.
eye-dropper-pick
resetnone
A form reset.
restorenone
The browser restored state, like after going back, or a saved value was read back from storage.
imperativenone
A controller or api call, or a property set on a web component.

Controller

Create one with createColorPickerController() and pass it as controller.

setValue() takes any CSS color and an optional { reason, event }, runs the change callback like a drag, and returns false when the color can't be read or the change is canceled.

getValue()() => string
The value, as a CSS color.
setValue(value, options?)(value: string, options?: ColorPickerChangeOptions | undefined) => boolean
Sets the value from any CSS color, as a user would. Returns false for text that isn't one.
focus()() => void
Moves focus to the area's thumb, or the first slider's.

Events

Callbacks get the new value first, then the details. A bound value only updates when onValueChange wasn't canceled.

PropOnReceives
bind:valueColorPickerThe value
onValueChangeColorPicker(value, details): the new value, the reason and details.color
onValueCommitColorPicker(value, details): the value once the interaction ends, its reason and details.color

Accessibility

No WAI-ARIA pattern covers a color picker, so it's built from ones that do. The picker is a group. The area's thumb and each slider's thumb follow the WAI-ARIA slider pattern, and the area's thumb names both of its channels, like "Saturation 76%, Brightness 96%". The presets follow the listbox pattern, and each one is named by its color, like "vivid red", as is ColorPickerSwatch.

The thumbs, inputs and presets are named in English by default. Pass aria-label to name them in another language.

Keyboard

KeyDoes
Arrow keysMove a thumb one step. On the area, Left and Right move across, Up and Down move up and down.
Shift with an arrowMoves ten steps
Page Up or Page DownMoves a tenth of the range, up the area or along a slider
Home or EndMoves to the start or end, across the area or along a slider
EnterReads the text in the input, or picks the focused preset
EscapePuts the input's text back to the value

Each key press commits, like the end of a drag.

Tested Behavior

Every line below is an automated test.

Value
  • The parts show the default value.
  • A press on the area moves the color there, and commits when it lifts.
  • A drag along a slider follows the pointer and commits once.
  • The text input reads any CSS color on Enter, and text that isn't one goes back.
  • A channel input sets its channel in its own units.
  • A controller sets the value, and a canceled change keeps it.
Keyboard
  • Arrows step a slider, Shift ten steps, and each press commits.
  • Arrows move the area's thumb on both axes.
  • Swatches are one tab stop, arrows move between them, and a press picks one.
Forms
  • The value submits under its name, and a reset returns to the default.
  • A disabled picker can't be focused or changed, and submits nothing.
Gamut
  • In OKLCH, a color past the sRGB edge writes the closest one inside, and the thumb moves there once it lifts.
  • The canvas paints the area, with the colors outside the gamut left clear.

Runs in React, Vue and web components.