# 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.

Canonical: https://hextaui.com/docs/ui-sdk-color-picker
Last updated: 2026-11-13

> **Beta.** Built on Vercel’s UI SDK 0.1.0-beta.1 (`@ui-sdk/react`), a public beta. Its API may change.

```tsx title="components/examples/ui-sdk-color-picker/demo.tsx"
"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 }
```

## Installation

### CLI

```bash
npx shadcn@latest add https://hextaui.com/r/ui-sdk-color-picker.json
```

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

### Manual

Add the HextaUI theme tokens (https://hextaui.com/docs/installation#theme) to your global CSS if you haven't yet, then install the dependencies.

```bash
pnpm add @ui-sdk/react@0.1.0-beta.1 @ui-sdk/core@0.1.0-beta.1 @tabler/icons-react cn
```

Copy and paste the following code into your project.

```tsx title="components/ui/ui-sdk-color-picker.tsx"
"use client"

import * as React from "react"
import { IconColorPicker } from "@tabler/icons-react"
import * as ColorPickerPrimitive from "@ui-sdk/react/color-picker"
import {
  Popover as PopoverPrimitive,
  PopoverContent as PopoverContentPrimitive,
  PopoverTrigger as PopoverTriggerPrimitive,
} from "@ui-sdk/react/popover"
import { cn } from "cn"

type ClassName<State> =
  string | ((state: State) => string | undefined) | undefined

function mergeClassName<State>(base: string, className: ClassName<State>) {
  return typeof className === "function"
    ? (state: State) => cn(base, className(state))
    : cn(base, className)
}

const thumbClassName =
  "absolute z-10 size-4 -translate-1/2 cursor-grab rounded-full border-2 border-white bg-(--color-picker-color) ring-(length:--hairline) ring-black/40 transition-[scale,box-shadow] duration-150 ease-out-quint outline-none select-none focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:outline-hidden data-disabled:cursor-not-allowed data-readonly:cursor-default motion-reduce:transition-none forced-colors:border-[CanvasText] forced-colors:bg-[Canvas] pointer-coarse:size-5"

const fieldClassName =
  "h-9 w-full min-w-0 rounded-md bg-transparent px-3 font-mono text-sm text-foreground tabular-nums inset-ring-(length:--hairline) inset-ring-input transition-[color,background-color,box-shadow] duration-150 ease-out-cubic outline-none selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:inset-ring-ring focus-visible:outline-hidden disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:inset-ring-destructive data-readonly:bg-muted/40 motion-reduce:transition-none dark:bg-input/30 dark:data-readonly:bg-input/20 forced-colors:border pointer-coarse:text-[max(16px,1rem)] [@media(hover:hover)]:hover:not-focus-visible:not-disabled:not-data-readonly:inset-ring-ring/70"

type ColorPickerProps = React.ComponentProps<
  typeof ColorPickerPrimitive.ColorPicker
>

function ColorPicker({ className, ...props }: ColorPickerProps) {
  return (
    <ColorPickerPrimitive.ColorPicker
      className={mergeClassName(
        "group/color-picker flex w-full max-w-64 min-w-0 flex-col gap-3 data-disabled:opacity-50",
        className
      )}
      {...props}
    />
  )
}

type ColorPickerAreaProps = React.ComponentProps<
  typeof ColorPickerPrimitive.ColorPickerArea
>

function ColorPickerArea({
  className,
  children = <ColorPickerAreaThumb />,
  ...props
}: ColorPickerAreaProps) {
  return (
    <ColorPickerPrimitive.ColorPickerArea
      className={mergeClassName(
        "group/color-picker-area relative isolate aspect-[4/3] w-full shrink-0 cursor-crosshair touch-none rounded-[var(--color-picker-radius,var(--radius-lg))] bg-(image:--color-area-background) inset-ring-(length:--hairline) inset-ring-black/10 select-none data-readonly:cursor-default dark:inset-ring-white/10 forced-colors:border data-disabled:cursor-not-allowed",
        className
      )}
      {...props}
    >
      {children}
    </ColorPickerPrimitive.ColorPickerArea>
  )
}

function ColorPickerAreaCanvas({
  className,
  ...props
}: React.ComponentProps<typeof ColorPickerPrimitive.ColorPickerAreaCanvas>) {
  return (
    <ColorPickerPrimitive.ColorPickerAreaCanvas
      className={mergeClassName(
        "pointer-events-none absolute inset-0 -z-10 size-full rounded-[inherit]",
        className
      )}
      {...props}
    />
  )
}

function ColorPickerAreaThumb({
  className,
  ...props
}: React.ComponentProps<typeof ColorPickerPrimitive.ColorPickerAreaThumb>) {
  return (
    <ColorPickerPrimitive.ColorPickerAreaThumb
      className={mergeClassName(
        cn(
          thumbClassName,
          "group-active/color-picker-area:cursor-grabbing motion-safe:group-active/color-picker-area:not-data-disabled:not-data-readonly:scale-125"
        ),
        className
      )}
      {...props}
    />
  )
}

type ColorPickerSliderProps = React.ComponentProps<
  typeof ColorPickerPrimitive.ColorPickerSlider
>

function ColorPickerSlider({
  className,
  children = <ColorPickerSliderThumb />,
  ...props
}: ColorPickerSliderProps) {
  return (
    <ColorPickerPrimitive.ColorPickerSlider
      className={mergeClassName(
        "group/color-picker-slider relative shrink-0 cursor-pointer touch-none rounded-full bg-(image:--color-slider-background) inset-ring-(length:--hairline) inset-ring-black/10 select-none before:absolute before:content-[''] data-readonly:cursor-default data-[channel=alpha]:[background:var(--color-slider-background),repeating-conic-gradient(var(--color-muted)_0_25%,var(--color-background)_0_50%)_0_0/0.5rem_0.5rem] data-[orientation=horizontal]:h-3 data-[orientation=horizontal]:w-full data-[orientation=horizontal]:before:-inset-y-1.5 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-32 data-[orientation=vertical]:w-3 data-[orientation=vertical]:before:-inset-x-1.5 dark:inset-ring-white/10 forced-colors:border pointer-coarse:data-[orientation=horizontal]:before:-inset-y-3 pointer-coarse:data-[orientation=vertical]:before:-inset-x-3 data-disabled:cursor-not-allowed",
        className
      )}
      {...props}
    >
      {children}
    </ColorPickerPrimitive.ColorPickerSlider>
  )
}

function ColorPickerSliderThumb({
  className,
  ...props
}: React.ComponentProps<typeof ColorPickerPrimitive.ColorPickerSliderThumb>) {
  return (
    <ColorPickerPrimitive.ColorPickerSliderThumb
      className={mergeClassName(
        cn(
          thumbClassName,
          "group-active/color-picker-slider:cursor-grabbing data-[orientation=horizontal]:top-1/2 data-[orientation=vertical]:left-1/2 motion-safe:group-active/color-picker-slider:not-data-disabled:not-data-readonly:scale-125"
        ),
        className
      )}
      {...props}
    />
  )
}

function ColorPickerSwatch({
  className,
  ...props
}: React.ComponentProps<typeof ColorPickerPrimitive.ColorPickerSwatch>) {
  return (
    <ColorPickerPrimitive.ColorPickerSwatch
      className={mergeClassName(
        "relative size-9 shrink-0 overflow-hidden rounded-md inset-ring-(length:--hairline) inset-ring-black/10 [background:linear-gradient(var(--color-picker-color),var(--color-picker-color)),repeating-conic-gradient(var(--color-muted)_0_25%,var(--color-background)_0_50%)_0_0/0.5rem_0.5rem] dark:inset-ring-white/15 forced-colors:border",
        className
      )}
      {...props}
    />
  )
}

function ColorPickerInput({
  className,
  ...props
}: React.ComponentProps<typeof ColorPickerPrimitive.ColorPickerInput>) {
  return (
    <ColorPickerPrimitive.ColorPickerInput
      dir="auto"
      autoComplete="off"
      spellCheck={false}
      className={mergeClassName(fieldClassName, className)}
      {...props}
    />
  )
}

function ColorPickerChannelInput({
  className,
  ...props
}: React.ComponentProps<typeof ColorPickerPrimitive.ColorPickerChannelInput>) {
  return (
    <ColorPickerPrimitive.ColorPickerChannelInput
      dir="auto"
      className={mergeClassName(
        cn(fieldClassName, "px-2 text-center"),
        className
      )}
      {...props}
    />
  )
}

function ColorPickerEyeDropper({
  className,
  children = <IconColorPicker aria-hidden="true" />,
  ...props
}: React.ComponentProps<typeof ColorPickerPrimitive.ColorPickerEyeDropper>) {
  return (
    <ColorPickerPrimitive.ColorPickerEyeDropper
      className={mergeClassName(
        "inline-flex size-9 shrink-0 items-center justify-center rounded-md text-muted-foreground inset-ring-(length:--hairline) inset-ring-input transition-[color,background-color,box-shadow,scale] duration-150 ease-out-cubic outline-none focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:inset-ring-ring focus-visible:outline-hidden active:not-disabled:text-foreground disabled:pointer-events-none disabled:opacity-50 data-unsupported:hidden motion-safe:active:not-disabled:scale-96 motion-reduce:transition-none dark:bg-input/30 forced-colors:border [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 [@media(hover:hover)]:hover:not-disabled:bg-muted [@media(hover:hover)]:hover:not-disabled:text-foreground",
        className
      )}
      {...props}
    >
      {children}
    </ColorPickerPrimitive.ColorPickerEyeDropper>
  )
}

function ColorPickerSwatches({
  className,
  ...props
}: React.ComponentProps<typeof ColorPickerPrimitive.ColorPickerSwatches>) {
  return (
    <ColorPickerPrimitive.ColorPickerSwatches
      className={mergeClassName(
        "flex flex-wrap gap-1.5 outline-none",
        className
      )}
      {...props}
    />
  )
}

function ColorPickerSwatchItem({
  className,
  ...props
}: React.ComponentProps<typeof ColorPickerPrimitive.ColorPickerSwatchItem>) {
  return (
    <ColorPickerPrimitive.ColorPickerSwatchItem
      className={mergeClassName(
        "relative size-6 shrink-0 cursor-pointer rounded-md inset-ring-(length:--hairline) inset-ring-black/10 ring-offset-2 ring-offset-background transition-[scale,box-shadow] duration-150 ease-out-quint outline-none [background:linear-gradient(var(--color-picker-color),var(--color-picker-color)),repeating-conic-gradient(var(--color-muted)_0_25%,var(--color-background)_0_50%)_0_0/0.5rem_0.5rem] focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:outline-hidden disabled:cursor-not-allowed data-readonly:cursor-default data-[selected]:ring-2 data-[selected]:ring-foreground motion-safe:active:not-data-disabled:not-data-readonly:scale-90 motion-reduce:transition-none dark:inset-ring-white/15 forced-colors:border forced-colors:data-[selected]:outline-2 forced-colors:data-[selected]:outline-offset-2 forced-colors:data-[selected]:outline-[Highlight] forced-colors:data-[selected]:outline-solid pointer-coarse:size-8 data-disabled:cursor-not-allowed [@media(hover:hover)]:hover:not-data-disabled:not-data-readonly:scale-110",
        className
      )}
      {...props}
    />
  )
}

function ColorPickerPopover(
  props: React.ComponentProps<typeof PopoverPrimitive>
) {
  return <PopoverPrimitive {...props} />
}

type ColorPickerTriggerProps = React.ComponentProps<
  typeof PopoverTriggerPrimitive
>

function ColorPickerTrigger({ className, ...props }: ColorPickerTriggerProps) {
  return (
    <PopoverTriggerPrimitive
      className={mergeClassName(
        "inline-flex h-9 max-w-full min-w-0 shrink-0 items-center gap-2 rounded-md bg-transparent ps-1.5 pe-3 text-sm text-foreground inset-ring-(length:--hairline) inset-ring-input transition-[color,background-color,box-shadow,scale] duration-150 ease-out-cubic outline-none focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:inset-ring-ring focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50 motion-safe:active:not-disabled:scale-98 motion-reduce:transition-none dark:bg-input/30 forced-colors:border data-open:inset-ring-ring [&>[data-slot=color-picker-swatch]]:size-6 [&>[data-slot=color-picker-swatch]]:rounded-[max(calc(var(--radius-sm)*0.5),calc(var(--radius-md)-var(--spacing)*1.5))] [@media(hover:hover)]:hover:not-disabled:bg-muted/60",
        className
      )}
      {...props}
    />
  )
}

function ColorPickerContent({
  className,
  sideOffset = 6,
  align = "start",
  ...props
}: React.ComponentProps<typeof PopoverContentPrimitive>) {
  return (
    <PopoverContentPrimitive
      sideOffset={sideOffset}
      align={align}
      className={mergeClassName(
        "m-0 flex max-h-(--available-height) w-64 max-w-(--available-width) origin-(--transform-origin) flex-col gap-3 overflow-y-auto overscroll-contain rounded-xl border-0 bg-popover p-2.5 text-sm text-popover-foreground shadow-md ring-(length:--hairline) ring-foreground/10 transition-[opacity,scale] duration-150 ease-out-quint outline-none [--color-picker-radius:calc(var(--radius-xl)-var(--spacing)*2.5)] data-ending-style:opacity-0 data-ending-style:duration-100 data-starting-style:opacity-0 motion-safe:data-ending-style:scale-96 motion-safe:data-starting-style:scale-96 motion-reduce:transition-opacity forced-colors:border",
        className
      )}
      {...props}
    />
  )
}

export {
  ColorPicker,
  ColorPickerArea,
  ColorPickerAreaCanvas,
  ColorPickerAreaThumb,
  ColorPickerChannelInput,
  ColorPickerContent,
  ColorPickerEyeDropper,
  ColorPickerInput,
  ColorPickerPopover,
  ColorPickerSlider,
  ColorPickerSliderThumb,
  ColorPickerSwatch,
  ColorPickerSwatches,
  ColorPickerSwatchItem,
  ColorPickerTrigger,
}
export { createColorPickerController } from "@ui-sdk/react/color-picker"
export type {
  ColorPickerProps,
  ColorPickerSliderProps,
  ColorPickerTriggerProps,
}
```

Update the import paths to match your project setup.

## Usage

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.

```css
@layer ui-sdk;

@import "tailwindcss";
```

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

```tsx
<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.

## Composition

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

## Examples

### Inline

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

```tsx title="components/examples/ui-sdk-color-picker/inline.tsx"
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 }
```

### Alpha

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.

```tsx title="components/examples/ui-sdk-color-picker/alpha.tsx"
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 }
```

### OKLCH

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.

```tsx title="components/examples/ui-sdk-color-picker/oklch.tsx"
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 }
```

### Presets and channels

`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.

```tsx title="components/examples/ui-sdk-color-picker/channels.tsx"
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 }
```

### Vertical sliders

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

```tsx title="components/examples/ui-sdk-color-picker/vertical.tsx"
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 }
```

### Controlled

`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.

```tsx title="components/examples/ui-sdk-color-picker/controlled.tsx"
"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 }
```

### Form

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

```tsx title="components/examples/ui-sdk-color-picker/form.tsx"
"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 }
```

### Disabled

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

```tsx title="components/examples/ui-sdk-color-picker/disabled.tsx"
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 }
```

### Read-only

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

```tsx title="components/examples/ui-sdk-color-picker/read-only.tsx"
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 }
```

### Right to left

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.

```tsx title="components/examples/ui-sdk-color-picker/rtl.tsx"
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 }
```

## Keyboard

| Key | Action |
| --- | --- |
| `←` `→` `↑` `↓` | Moves a thumb one step. On the area, left and right move across, up and down move up and down. |
| `Shift + arrow` | Moves ten steps. |
| `Page Up` `Page Down` | Moves a tenth of the range, up the area or along a slider. |
| `Home` `End` | Moves to the start or the end. |
| `Enter` | Reads the text in the input, or picks the focused preset. |
| `Esc` | Puts the input’s text back to the value, or closes the popover. |

## Accessibility

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.

## API reference

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.

### ColorPicker

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `string` | – |  |
| `defaultValue` | `string` | `"black"` |  |
| `onValueChange` | `(value: string, details: ChangeDetails) => void` | – | Runs on every step of a drag, a key press, a typed color or a preset. |
| `onValueCommit` | `(value: string, details: ChangeDetails) => void` | – | Runs once when a drag, key or typed color ends. |
| `space` | `"hsv" \| "hsl" \| "srgb" \| "oklch"` | `"hsv"` | Which channels the parts edit. |
| `format` | `ColorFormat` | – | How the value is written. Hex for hsv, the space’s own CSS syntax otherwise. |
| `gamut` | `"srgb" \| "p3" \| false` | `"srgb"` |  |
| `disabled` | `boolean` | `false` |  |
| `readOnly` | `boolean` | `false` |  |
| `required` | `boolean` | `false` |  |
| `name` | `string` | – |  |
| `form` | `string` | – |  |
| `controller` | `ColorPickerController` | – | From createColorPickerController(). |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<div>` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="color-picker"` | Target the root in CSS. |
| `data-disabled` | Present when disabled. |
| `data-readonly` | Present when read-only. |
| `data-required` | Present when required. |
| `data-out-of-gamut` | Present on every part while a drag holds a color outside the gamut. |
| `--color-picker-color` | The color as CSS. Thumbs and swatches fill with it. |

### ColorPickerArea

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `x` | `ColorChannel` | – | The channel across. Saturation, red or chroma, by space. |
| `y` | `ColorChannel` | – | The channel up. Brightness, lightness or green, by space. |
| `children` | `ReactNode` | `<ColorPickerAreaThumb />` |  |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<div>` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="color-picker-area"` | Target the area in CSS. |
| `--color-area-background` | The gradients that draw the area. Unset where a canvas paints it. |
| `--color-picker-radius` | The area’s corner radius. Set by ColorPickerContent so its corners are concentric. |

### ColorPickerAreaThumb

| Attribute | Description |
| --- | --- |
| `data-slot="color-picker-area-thumb"` | Target the thumb in CSS. |
| `--area-x` | Where the thumb is across the area, from 0 to 1. |
| `--area-y` | Where the thumb is up the area, from 0 to 1. |

### ColorPickerSlider

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `channel` | `ColorChannel` | – | Hue, or blue in sRGB, by default. |
| `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` |  |
| `children` | `ReactNode` | `<ColorPickerSliderThumb />` |  |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<div>` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="color-picker-slider"` | Target the slider in CSS. |
| `data-channel` | The channel it edits, like hue or alpha. |
| `data-orientation` | horizontal or vertical. |
| `--color-slider-background` | A gradient through the channel. |

### ColorPickerChannelInput

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `channel` | `ColorChannel` | – | The channel to edit, in the units people read it in. |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<input>` |  |

### ColorPickerSwatchItem

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `string` | – | Required. The preset’s color. |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<button>` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="color-picker-swatch-item"` | Target presets in CSS. |
| `data-selected` | Present while the value is this preset’s color. |

### ColorPickerEyeDropper

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | `<IconColorPicker />` |  |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<button>` |  |

| Attribute | Description |
| --- | --- |
| `data-unsupported` | Present where the browser has no EyeDropper. The button is hidden then. |

### ColorPickerContent

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

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `side` | `"top" \| "bottom" \| "left" \| "right" \| "start" \| "end"` | `"bottom"` |  |
| `align` | `"start" \| "center" \| "end"` | `"start"` |  |
| `sideOffset` | `number` | `6` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="popover-content"` | Set by UI SDK on the popup. Target it in CSS. |
| `data-starting-style` | Present while the popup animates in. |
| `data-ending-style` | Present while the popup animates out. |

## Notes for AI assistants

- Install a component with the shadcn CLI: `npx shadcn@latest add https://hextaui.com/r/<name>.json`. It adds the source, the HextaUI theme tokens and any HextaUI components it depends on. `https://hextaui.com/r/all.json` installs every component.
- The code is then owned by the project, like shadcn/ui. There is no HextaUI npm package. HextaUI is MIT licensed and free for personal and commercial use.
- Behavior and accessibility come from Base UI (`@base-ui/react`). Compose with the `render` prop, not `asChild`.
- Styling uses Tailwind CSS v4 with theme tokens. Merge classes with `cn` from the `cn` package.
- Icons come from `@tabler/icons-react`.
- Import components from `@/components/ui/<name>`, hooks from `@/hooks/<name>` and utilities from `@/lib/<name>`.

Every HextaUI doc: https://hextaui.com/llms.txt

## Related

- [Calendar](https://hextaui.com/docs/calendar) - A date grid for single, range and multiple selection, with sliding months, range previews and touch-sized days.
- [Checkbox](https://hextaui.com/docs/checkbox) - A checkbox whose check draws in, with indeterminate parents, groups and labels that share its hover.
- [Combobox](https://hextaui.com/docs/combobox) - A filterable select with chips, groups and async results, in a popup that resizes as you type.
- [Date picker](https://hextaui.com/docs/date-picker) - A button that opens a calendar in a popover, or a bottom sheet on phones, for single dates and ranges.
- [Field](https://hextaui.com/docs/field) - Labels, descriptions and errors wired to their control, with validation states and layouts for forms.
- [Input](https://hextaui.com/docs/input) - A text input with three sizes, invalid and read-only states, native validation styling and a 16px touch font so phones never zoom in.
