HextaUI

BetaBuilt on Vercel’s UI SDK 0.1.0-beta.1. Its API may change.

Color picker

A color picker on Vercel's UI SDK with a saturation area, hue and alpha sliders, typed CSS colors, presets and an eyedropper.

Depends on
  • @tabler/icons-react
  • @ui-sdk/core
  • @ui-sdk/react
"use client"

import * as React from "react"

import {
  ColorPicker,
  ColorPickerArea,
  ColorPickerContent,
  ColorPickerEyeDropper,
  ColorPickerInput,
  ColorPickerPopover,
  ColorPickerSlider,
  ColorPickerSwatch,
  ColorPickerSwatches,
  ColorPickerSwatchItem,
  ColorPickerTrigger,
} from "@/components/ui/ui-sdk-color-picker"

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

function UiSdkColorPickerDemo() {
  const [value, setValue] = React.useState("#6366f1")

  return (
    <ColorPicker
      aria-label="Accent color"
      value={value}
      onValueChange={setValue}
    >
      <ColorPickerPopover>
        <ColorPickerTrigger aria-label={`Accent color, ${value}`}>
          <ColorPickerSwatch />
          <span className="font-mono text-xs uppercase tabular-nums">
            {value}
          </span>
        </ColorPickerTrigger>
        <ColorPickerContent>
          <ColorPickerArea />
          <ColorPickerSlider channel="hue" />
          <div className="flex items-center gap-2">
            <ColorPickerInput aria-label="Hex" />
            <ColorPickerEyeDropper aria-label="Pick a color from the screen" />
          </div>
          <ColorPickerSwatches aria-label="Presets">
            {presets.map((preset) => (
              <ColorPickerSwatchItem key={preset} value={preset} />
            ))}
          </ColorPickerSwatches>
        </ColorPickerContent>
      </ColorPickerPopover>
    </ColorPicker>
  )
}

export { UiSdkColorPickerDemo }
pnpm dlx shadcn@latest add https://hextaui.com/r/ui-sdk-color-picker.json

Adds the component, the HextaUI theme tokens and any HextaUI components it depends on.

UI SDK adds a little CSS of its own in a cascade layer named ui-sdk. Declare that layer at the top of your global stylesheet, above the Tailwind import, so your classes win over it.

@layer ui-sdk;

@import "tailwindcss";
import {
  ColorPicker,
  ColorPickerArea,
  ColorPickerInput,
  ColorPickerSlider,
  ColorPickerSwatch,
} from "@/components/ui/ui-sdk-color-picker"
<ColorPicker aria-label="Brand color" defaultValue="#0ea5e9">
  <ColorPickerArea />
  <ColorPickerSlider channel="hue" />
  <ColorPickerSwatch />
  <ColorPickerInput aria-label="Hex" />
</ColorPicker>

The value is a CSS color string. Every part edits the same color, so use only the parts you need. ColorPickerArea and ColorPickerSlider render their thumb for you. Name the picker with aria-label, or with the label of a field around it.

ColorPicker
├── ColorPickerArea
│   ├── ColorPickerAreaCanvas
│   └── ColorPickerAreaThumb
├── ColorPickerSlider
│   └── ColorPickerSliderThumb
├── ColorPickerSwatch
├── ColorPickerInput
├── ColorPickerChannelInput
├── ColorPickerEyeDropper
├── ColorPickerSwatches
│   └── ColorPickerSwatchItem
└── ColorPickerPopover
    ├── ColorPickerTrigger
    └── ColorPickerContent

Render the parts in place, without a popover, for a settings page or a sidebar.

import {
  ColorPicker,
  ColorPickerArea,
  ColorPickerInput,
  ColorPickerSlider,
  ColorPickerSwatch,
} from "@/components/ui/ui-sdk-color-picker"

function UiSdkColorPickerInline() {
  return (
    <ColorPicker aria-label="Brand color" defaultValue="#0ea5e9">
      <ColorPickerArea />
      <ColorPickerSlider channel="hue" />
      <div className="flex items-center gap-2">
        <ColorPickerSwatch />
        <ColorPickerInput aria-label="Hex" />
      </div>
    </ColorPicker>
  )
}

export { UiSdkColorPickerInline }

A slider with channel="alpha" edits opacity. Its track and the swatch draw a checkerboard under the color, so a see-through color reads as see-through.

import {
  ColorPicker,
  ColorPickerArea,
  ColorPickerInput,
  ColorPickerSlider,
  ColorPickerSwatch,
} from "@/components/ui/ui-sdk-color-picker"

function UiSdkColorPickerAlpha() {
  return (
    <ColorPicker
      aria-label="Overlay color"
      space="hsl"
      defaultValue="hsl(262 83% 58% / 0.6)"
    >
      <ColorPickerArea />
      <ColorPickerSlider channel="hue" />
      <ColorPickerSlider channel="alpha" />
      <div className="flex items-center gap-2">
        <ColorPickerSwatch />
        <ColorPickerInput aria-label="HSL" />
      </div>
    </ColorPicker>
  )
}

export { UiSdkColorPickerAlpha }

With space="oklch", add ColorPickerAreaCanvas to paint the area. Colors outside the screen’s gamut stay clear, and a thumb dragged there settles on the closest color inside when you let go.

import {
  ColorPicker,
  ColorPickerArea,
  ColorPickerAreaCanvas,
  ColorPickerAreaThumb,
  ColorPickerInput,
  ColorPickerSlider,
  ColorPickerSwatch,
} from "@/components/ui/ui-sdk-color-picker"

function UiSdkColorPickerOklch() {
  return (
    <ColorPicker
      aria-label="Accent"
      space="oklch"
      defaultValue="oklch(0.62 0.19 259)"
    >
      <ColorPickerArea>
        <ColorPickerAreaCanvas />
        <ColorPickerAreaThumb />
      </ColorPickerArea>
      <ColorPickerSlider channel="hue" />
      <div className="flex items-center gap-2">
        <ColorPickerSwatch />
        <ColorPickerInput aria-label="OKLCH" />
      </div>
    </ColorPicker>
  )
}

export { UiSdkColorPickerOklch }

ColorPickerSwatches is one tab stop with arrow keys between presets, and a preset stays selected however the value is written. ColorPickerChannelInput edits one channel as a number. The eyedropper hides itself in browsers that can’t pick from the screen.

import {
  ColorPicker,
  ColorPickerChannelInput,
  ColorPickerEyeDropper,
  ColorPickerSwatch,
  ColorPickerSwatches,
  ColorPickerSwatchItem,
} from "@/components/ui/ui-sdk-color-picker"

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

const channels = [
  { channel: "red", label: "R" },
  { channel: "green", label: "G" },
  { channel: "blue", label: "B" },
] as const

function UiSdkColorPickerChannels() {
  return (
    <ColorPicker
      aria-label="Label color"
      space="srgb"
      format="hex"
      defaultValue="#0ea5e9"
    >
      <ColorPickerSwatches aria-label="Presets">
        {presets.map((preset) => (
          <ColorPickerSwatchItem key={preset} value={preset} />
        ))}
      </ColorPickerSwatches>
      <div className="flex items-end gap-2">
        <ColorPickerSwatch />
        {channels.map(({ channel, label }) => (
          <label
            key={channel}
            className="flex min-w-0 flex-1 flex-col gap-1 text-xs text-muted-foreground"
          >
            {label}
            <ColorPickerChannelInput channel={channel} />
          </label>
        ))}
        <ColorPickerEyeDropper aria-label="Pick a color from the screen" />
      </div>
    </ColorPicker>
  )
}

export { UiSdkColorPickerChannels }

Set orientation="vertical" on a slider to stand it beside the area.

import {
  ColorPicker,
  ColorPickerArea,
  ColorPickerSlider,
} from "@/components/ui/ui-sdk-color-picker"

function UiSdkColorPickerVertical() {
  return (
    <ColorPicker aria-label="Highlight" space="hsl" defaultValue="#f97316">
      <div className="flex gap-3">
        <div className="min-w-0 flex-1">
          <ColorPickerArea />
        </div>
        <ColorPickerSlider channel="hue" orientation="vertical" />
        <ColorPickerSlider channel="alpha" orientation="vertical" />
      </div>
    </ColorPicker>
  )
}

export { UiSdkColorPickerVertical }

onValueChange runs on every step of a drag, and onValueCommit once when it ends, so save there. A controller from createColorPickerController() sets the color from anywhere.

"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import {
  ColorPicker,
  ColorPickerArea,
  ColorPickerInput,
  ColorPickerSlider,
  createColorPickerController,
} from "@/components/ui/ui-sdk-color-picker"

function UiSdkColorPickerControlled() {
  const [picker] = React.useState(() => createColorPickerController())
  const [value, setValue] = React.useState("#0ea5e9")
  const [saved, setSaved] = React.useState("#0ea5e9")

  return (
    <div className="flex w-full max-w-64 flex-col gap-3">
      <ColorPicker
        aria-label="Theme color"
        value={value}
        onValueChange={setValue}
        onValueCommit={setSaved}
        controller={picker}
      >
        <ColorPickerArea />
        <ColorPickerSlider channel="hue" />
        <ColorPickerInput aria-label="Hex" />
      </ColorPicker>
      <div className="flex items-center justify-between gap-2 text-sm">
        <p className="text-muted-foreground">
          Saved <span className="font-mono text-foreground">{saved}</span>
        </p>
        <Button
          variant="outline"
          size="sm"
          onClick={() => picker.setValue("#663399")}
        >
          Use purple
        </Button>
      </div>
    </div>
  )
}

export { UiSdkColorPickerControlled }

A hidden input submits the value under name, and a form reset goes back to defaultValue.

"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import {
  ColorPicker,
  ColorPickerArea,
  ColorPickerSlider,
  ColorPickerSwatch,
} from "@/components/ui/ui-sdk-color-picker"

function UiSdkColorPickerForm() {
  const [submitted, setSubmitted] = React.useState<string | null>(null)

  return (
    <form
      className="flex w-full max-w-64 flex-col gap-3"
      onSubmit={(event) => {
        event.preventDefault()
        setSubmitted(String(new FormData(event.currentTarget).get("brand")))
      }}
      onReset={() => setSubmitted(null)}
    >
      <ColorPicker aria-label="Brand color" name="brand" defaultValue="#f97316">
        <ColorPickerArea />
        <div className="flex items-center gap-2">
          <ColorPickerSwatch />
          <div className="min-w-0 flex-1">
            <ColorPickerSlider channel="hue" />
          </div>
        </div>
      </ColorPicker>
      <div className="flex gap-2">
        <Button type="submit" size="sm">
          Save
        </Button>
        <Button type="reset" variant="outline" size="sm">
          Reset
        </Button>
      </div>
      <p role="status" className="text-sm text-muted-foreground">
        {submitted ? (
          <>
            Submitted brand=
            <span className="font-mono text-foreground">{submitted}</span>
          </>
        ) : (
          "Nothing submitted yet."
        )}
      </p>
    </form>
  )
}

export { UiSdkColorPickerForm }

A disabled picker can’t be focused or changed, and submits nothing.

import {
  ColorPicker,
  ColorPickerArea,
  ColorPickerInput,
  ColorPickerSlider,
} from "@/components/ui/ui-sdk-color-picker"

function UiSdkColorPickerDisabled() {
  return (
    <ColorPicker aria-label="Brand color" defaultValue="#0ea5e9" disabled>
      <ColorPickerArea />
      <ColorPickerSlider channel="hue" />
      <ColorPickerInput aria-label="Hex" />
    </ColorPicker>
  )
}

export { UiSdkColorPickerDisabled }

With readOnly, the thumbs stay focusable and the value is submitted, but drags, keys and typing leave the color alone.

import {
  ColorPicker,
  ColorPickerArea,
  ColorPickerInput,
  ColorPickerSlider,
  ColorPickerSwatch,
} from "@/components/ui/ui-sdk-color-picker"

function UiSdkColorPickerReadOnly() {
  return (
    <ColorPicker aria-label="Theme color" defaultValue="#22c55e" readOnly>
      <ColorPickerArea />
      <ColorPickerSlider channel="hue" />
      <div className="flex items-center gap-2">
        <ColorPickerSwatch />
        <ColorPickerInput aria-label="Hex" />
      </div>
    </ColorPicker>
  )
}

export { UiSdkColorPickerReadOnly }

UI SDK reads the direction from the page, so the area, the sliders and the arrow keys mirror. The inputs pick their own direction, so a hex value still reads left to right.

import {
  ColorPicker,
  ColorPickerArea,
  ColorPickerInput,
  ColorPickerSlider,
  ColorPickerSwatch,
} from "@/components/ui/ui-sdk-color-picker"

function UiSdkColorPickerRtl() {
  return (
    <div dir="rtl" className="flex w-full justify-center">
      <ColorPicker aria-label="لون العلامة" defaultValue="#ec4899">
        <ColorPickerArea />
        <ColorPickerSlider channel="hue" />
        <div className="flex items-center gap-2">
          <ColorPickerSwatch />
          <ColorPickerInput aria-label="سداسي عشري" />
        </div>
      </ColorPicker>
    </div>
  )
}

export { UiSdkColorPickerRtl }
KeyAction
←→↑↓Moves a thumb one step. On the area, left and right move across, up and down move up and down.
Shift + arrowMoves ten steps.
Page UpPage DownMoves a tenth of the range, up the area or along a slider.
HomeEndMoves to the start or the end.
EnterReads the text in the input, or picks the focused preset.
EscPuts the input’s text back to the value, or closes the popover.

No WAI-ARIA pattern covers a color picker, so UI SDK builds it from ones that do. The picker is a group. The area’s thumb and each slider thumb are sliders, and the area’s thumb names both of its channels, like “Saturation 76%, Brightness 96%”. The presets are a listbox, and each preset and the swatch are named by their color, like “vivid blue”.

The trigger in the popover example says the current color in its name. Thumbs grow while you drag only when motion is allowed, and the checkerboard, rings and focus outlines switch to system colors in forced colors mode.

Each part wraps the UI SDK part of the same name and accepts all of its props, including render and a className that can be a function of the part’s state.

PropTypeDefault
value
string–
defaultValue
string"black"
onValueChangeRuns on every step of a drag, a key press, a typed color or a preset.
(value: string, details: ChangeDetails) => void–
onValueCommitRuns once when a drag, key or typed color ends.
(value: string, details: ChangeDetails) => void–
spaceWhich channels the parts edit.
"hsv" | "hsl" | "srgb" | "oklch""hsv"
formatHow the value is written. Hex for hsv, the space’s own CSS syntax otherwise.
ColorFormat–
gamut
"srgb" | "p3" | false"srgb"
disabled
booleanfalse
readOnly
booleanfalse
required
booleanfalse
name
string–
form
string–
controllerFrom createColorPickerController().
ColorPickerController–
render
ReactElement | (props, state) => ReactElement<div>
AttributeDescription
data-slot="color-picker"Target the root in CSS.
data-disabledPresent when disabled.
data-readonlyPresent when read-only.
data-requiredPresent when required.
data-out-of-gamutPresent on every part while a drag holds a color outside the gamut.
--color-picker-colorThe color as CSS. Thumbs and swatches fill with it.
PropTypeDefault
xThe channel across. Saturation, red or chroma, by space.
ColorChannel–
yThe channel up. Brightness, lightness or green, by space.
ColorChannel–
children
ReactNode<ColorPickerAreaThumb />
render
ReactElement | (props, state) => ReactElement<div>
AttributeDescription
data-slot="color-picker-area"Target the area in CSS.
--color-area-backgroundThe gradients that draw the area. Unset where a canvas paints it.
--color-picker-radiusThe area’s corner radius. Set by ColorPickerContent so its corners are concentric.
AttributeDescription
data-slot="color-picker-area-thumb"Target the thumb in CSS.
--area-xWhere the thumb is across the area, from 0 to 1.
--area-yWhere the thumb is up the area, from 0 to 1.
PropTypeDefault
channelHue, or blue in sRGB, by default.
ColorChannel–
orientation
"horizontal" | "vertical""horizontal"
children
ReactNode<ColorPickerSliderThumb />
render
ReactElement | (props, state) => ReactElement<div>
AttributeDescription
data-slot="color-picker-slider"Target the slider in CSS.
data-channelThe channel it edits, like hue or alpha.
data-orientationhorizontal or vertical.
--color-slider-backgroundA gradient through the channel.
PropTypeDefault
channelThe channel to edit, in the units people read it in.
ColorChannel–
render
ReactElement | (props, state) => ReactElement<input>
PropTypeDefault
valueRequired. The preset’s color.
string–
render
ReactElement | (props, state) => ReactElement<button>
AttributeDescription
data-slot="color-picker-swatch-item"Target presets in CSS.
data-selectedPresent while the value is this preset’s color.
PropTypeDefault
children
ReactNode<IconColorPicker />
render
ReactElement | (props, state) => ReactElement<button>
AttributeDescription
data-unsupportedPresent where the browser has no EyeDropper. The button is hidden then.

ColorPickerPopover, ColorPickerTrigger and ColorPickerContent wrap the UI SDK popover. The content opens in the browser’s top layer, so it needs no portal.

PropTypeDefault
side
"top" | "bottom" | "left" | "right" | "start" | "end""bottom"
align
"start" | "center" | "end""start"
sideOffset
number6
AttributeDescription
data-slot="popover-content"Set by UI SDK on the popup. Target it in CSS.
data-starting-stylePresent while the popup animates in.
data-ending-stylePresent while the popup animates out.