import { ColorPicker } from "@/components/ui/color-picker" ;
export default function ColorPickerDemo () {
return (
< ColorPicker
label = "Brand color"
defaultValue = "#6366f1"
swatches = {[
"#ef4444" ,
"#f97316" ,
"#eab308" ,
"#22c55e" ,
"#06b6d4" ,
"#3b82f6" ,
"#6366f1" ,
"#a855f7" ,
]}
/>
);
}
CLI Manual
$ pnpm dlx shadcn@latest add @desyne/color-picker
The CLI installs dependencies and any other components this one uses.
import { ColorPicker } from "@/components/ui/color-picker" ;
< ColorPicker label = "Accent" defaultValue = "#6366f1" swatches = {[ "#ef4444" , "#22c55e" ]} />
ColorPicker is a complete control. For a custom popover, pass the parts as children. For an inline editor, wrap the parts in React Aria's ColorPicker, which keeps one shared color in sync between them:
import { ColorPicker as ColorPickerState } from "react-aria-components" ;
import { ColorArea, ColorField, ColorSlider } from "@/components/ui/color-picker" ;
< ColorPickerState defaultValue = "#6366f1" >
< ColorArea colorSpace = "hsb" xChannel = "saturation" yChannel = "brightness" />
< ColorSlider label = "Hue" colorSpace = "hsb" channel = "hue" />
< ColorField label = "Hex" />
</ ColorPickerState >;
Values can be passed as any CSS color string ("#6366f1", "rgb(…)", "hsl(…)") or a Color object from parseColor. onChange always receives a Color; call color.toString("hex") (or "rgb", "hsl", "css"…) to serialize it.
Color Picker — any color, when users need precise control or a custom brand color.
ColorSwatchPicker on its own — a fixed palette, such as label or calendar colors. Faster and keeps colors consistent.
ColorField — users who already know the hex value, like designers pasting from a design tool.
ColorSwatch — displaying a color that isn't editable.
< ColorPicker > { /* trigger button + popover dialog */ }
< ColorArea /> { /* 2D saturation × brightness, with a ColorThumb */ }
< ColorSlider /> { /* one channel: hue, alpha, red… */ }
< ColorField /> { /* hex or single-channel text input */ }
< ColorSwatchPicker > { /* preset list */ }
< ColorSwatchPickerItem /> { /* one preset, renders a ColorSwatch */ }
</ ColorSwatchPicker >
</ ColorPicker >
Part Renders Notes ColorPicker<button> + popover role="dialog"Outline button with a swatch and label. The dialog holds the default editor, or your children. With name, also a hidden <input> after the button. ColorArea<div> + two hidden <input type="range">Drags two channels at once. Defaults to 192px square; add w-full to fill its container. ColorSlider<div role="group">Label and value output (when label is set) above a 12px gradient track. ColorThumb<div>The white ring handle used by ColorArea and ColorSlider. Exported for custom compositions. ColorField<div> + <input>Monospace text input with optional label, description and error. ColorSwatch<div role="img">A static color preview, labelled with a human-readable color name. ColorSwatchPicker<div role="listbox">A wrapping row of selectable presets. ColorSwatchPickerItem<div role="option">One preset; shows a ring when hovered, focused or selected.
Pass a Color as value and update it in onChange. Keep the Color object in state and serialize it only when you need a string.
Accenthex #0EA5E9 rgb rgb(14, 165, 233) hsl hsl(198.63, 88.66%, 48.43%)
import { useState } from "react" ;
import { type Color, parseColor } from "react-aria-components" ;
import { ColorPicker } from "@/components/ui/color-picker" ;
export default function ColorPickerControlled () {
const [ color , setColor ] = useState < Color >( parseColor ( "#0ea5e9" ));
return (
< div className = "flex flex-col items-start gap-3" >
< ColorPicker label = "Accent" value = {color} onChange = {setColor} />
< dl className = "grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 font-mono text-xs" >
< dt className = "text-muted-foreground" >hex</ dt >
< dd >{color. toString ( "hex" )}</ dd >
< dt className = "text-muted-foreground" >rgb</ dt >
< dd >{color. toString ( "rgb" )}</ dd >
< dt className = "text-muted-foreground" >hsl</ dt >
< dd >{color. toString ( "hsl" )}</ dd >
</ dl >
</ div >
);
}
children replace the default popover content. Here an alpha slider is added so users can pick translucent colors; the parts inside still share the picker's value.
import {
ColorArea,
ColorField,
ColorPicker,
ColorSlider,
ColorSwatchPicker,
ColorSwatchPickerItem,
} from "@/components/ui/color-picker" ;
const presets = [ "#0f172a80" , "#2563eb" , "#16a34acc" , "#f59e0b" , "#e11d4899" ];
export default function ColorPickerCustomContent () {
return (
< ColorPicker label = "Overlay" defaultValue = "#2563ebb3" >
< ColorArea
colorSpace = "hsb"
xChannel = "saturation"
yChannel = "brightness"
className = "w-full"
/>
< ColorSlider label = "Hue" colorSpace = "hsb" channel = "hue" />
< ColorSlider label = "Opacity" colorSpace = "hsb" channel = "alpha" />
< ColorField aria-label = "Hex" />
< ColorSwatchPicker aria-label = "Presets" >
{presets. map (( color ) => (
< ColorSwatchPickerItem key = {color} color = {color} />
))}
</ ColorSwatchPicker >
</ ColorPicker >
);
}
The same parts without a popover, wrapped in React Aria's ColorPicker. Use it in sidebars and design tools where the editor is always visible.
import { ColorPicker as ColorPickerState } from "react-aria-components" ;
import {
ColorArea,
ColorField,
ColorSlider,
ColorSwatch,
} from "@/components/ui/color-picker" ;
export default function ColorAreaSlider () {
return (
< ColorPickerState defaultValue = "hsl(200, 80%, 50%)" >
< div className = "flex w-full max-w-56 flex-col gap-3" >
< ColorArea
colorSpace = "hsb"
xChannel = "saturation"
yChannel = "brightness"
className = "w-full"
/>
< ColorSlider label = "Hue" colorSpace = "hsb" channel = "hue" />
< ColorSlider label = "Alpha" colorSpace = "hsb" channel = "alpha" />
< div className = "flex items-end gap-2" >
< ColorField label = "Hex" className = "flex-1" />
< ColorSwatch className = "size-8" />
</ div >
</ div >
</ ColorPickerState >
);
}
ColorSlider edits one channel in a colorSpace: rgb (red, green, blue), hsl (hue, saturation, lightness), hsb (hue, saturation, brightness), plus alpha. The track previews the gradient for that channel.
import { ColorPicker as ColorPickerState } from "react-aria-components" ;
import { ColorSlider, ColorSwatch } from "@/components/ui/color-picker" ;
export default function ColorChannelSliders () {
return (
< ColorPickerState defaultValue = "rgb(234, 88, 12)" >
< div className = "flex w-full max-w-60 flex-col gap-4" >
< div className = "h-10 w-full" >
< ColorSwatch className = "size-full" />
</ div >
< ColorSlider label = "Red" colorSpace = "rgb" channel = "red" />
< ColorSlider label = "Green" colorSpace = "rgb" channel = "green" />
< ColorSlider label = "Blue" colorSpace = "rgb" channel = "blue" />
< ColorSlider label = "Lightness" colorSpace = "hsl" channel = "lightness" />
</ div >
</ ColorPickerState >
);
}
A ColorField with a channel edits a single number instead of the hex string, so users can type exact RGB or HSL values.
import { ColorPicker as ColorPickerState } from "react-aria-components" ;
import {
ColorField,
ColorSlider,
ColorSwatch,
} from "@/components/ui/color-picker" ;
export default function ColorChannelFields () {
return (
< ColorPickerState defaultValue = "#7c3aed" >
< div className = "flex w-full max-w-64 flex-col gap-3" >
< div className = "flex items-center gap-3" >
< ColorSwatch className = "size-8" />
< ColorSlider
aria-label = "Hue"
colorSpace = "hsl"
channel = "hue"
className = "flex-1"
/>
</ div >
< div className = "grid grid-cols-3 gap-2" >
< ColorField label = "R" colorSpace = "rgb" channel = "red" />
< ColorField label = "G" colorSpace = "rgb" channel = "green" />
< ColorField label = "B" colorSpace = "rgb" channel = "blue" />
</ div >
< ColorField label = "Hex" />
</ div >
</ ColorPickerState >
);
}
A standalone hex input. The value is parsed when the user presses Enter or leaves the field, and arrow keys step it. isInvalid with errorMessage shows your own validation, such as a contrast check.
import { ColorField } from "@/components/ui/color-picker" ;
export default function ColorFieldDemo () {
return (
< div className = "flex w-full max-w-60 flex-col gap-5" >
< ColorField
label = "Brand color"
defaultValue = "#4f46e5"
description = "Hex, with or without the #. Applied when you press Enter or leave the field."
/>
< ColorField
label = "Link color"
defaultValue = "#fde047"
isInvalid
errorMessage = "Too little contrast against a white background (1.3:1)."
/>
</ div >
);
}
ColorSwatchPicker is a single-select list of presets with its own value, defaultValue and onChange. Give it an aria-label describing what the color is for.
import {
ColorSwatchPicker,
ColorSwatchPickerItem,
} from "@/components/ui/color-picker" ;
const colors = [
"#0f172a" ,
"#dc2626" ,
"#ea580c" ,
"#16a34a" ,
"#0891b2" ,
"#2563eb" ,
"#7c3aed" ,
"#db2777" ,
];
export default function ColorSwatches () {
return (
< ColorSwatchPicker aria-label = "Label color" defaultValue = "#2563eb" >
{colors. map (( color ) => (
< ColorSwatchPickerItem key = {color} color = {color} />
))}
</ ColorSwatchPicker >
);
}
ColorSwatch displays a color. It's announced by its color name (e.g. "dark vibrant blue"); pass colorName to use your own palette's name instead.
Ink #0f172a Ocean #0369a1 Moss #4d7c0f Clay #c2410c Rose #be123c Frost #e0f2fe
import { ColorSwatch } from "@/components/ui/color-picker" ;
const palette = [
{ name: "Ink" , value: "#0f172a" },
{ name: "Ocean" , value: "#0369a1" },
{ name: "Moss" , value: "#4d7c0f" },
{ name: "Clay" , value: "#c2410c" },
{ name: "Rose" , value: "#be123c" },
{ name: "Frost" , value: "#e0f2fe" },
];
export default function ColorSwatchDemo () {
return (
< ul className = "grid w-full max-w-md grid-cols-3 gap-3 sm:grid-cols-6" >
{palette. map (( c ) => (
< li key = {c.value} className = "flex flex-col gap-1.5" >
< div className = "aspect-square w-full" >
< ColorSwatch
color = {c.value}
colorName = {c.name}
className = "size-full rounded-lg"
/>
</ div >
< span className = "font-medium text-xs" >{c.name}</ span >
< span className = "font-mono text-[0.7rem] text-muted-foreground uppercase" >
{c.value}
</ span >
</ li >
))}
</ ul >
);
}
isDisabled on ColorPicker disables the trigger, keeps the popover closed and leaves its value out of form submissions. On ColorArea and ColorSlider it dims them and removes them from the tab order. ColorField supports both isDisabled and isReadOnly.
import { ColorPicker as ColorPickerState } from "react-aria-components" ;
import {
ColorArea,
ColorField,
ColorPicker,
ColorSlider,
} from "@/components/ui/color-picker" ;
export default function ColorPickerDisabled () {
return (
< div className = "flex w-full max-w-56 flex-col gap-5" >
< ColorPicker label = "Locked brand" defaultValue = "#0d9488" isDisabled />
< ColorPickerState defaultValue = "#0d9488" >
< div className = "flex flex-col gap-3" >
< ColorArea
colorSpace = "hsb"
xChannel = "saturation"
yChannel = "brightness"
className = "w-full"
isDisabled
/>
< ColorSlider label = "Hue" colorSpace = "hsb" channel = "hue" isDisabled />
< ColorField label = "Hex" isReadOnly />
</ div >
</ ColorPickerState >
</ div >
);
}
ColorField takes a name and submits its hex value. ColorPicker takes a name too: it renders a hidden input next to the trigger, inside your form, so it submits even though the editor lives in a popover. The value is an uppercase #RRGGBB hex string by default; valueFormat picks another format, such as "hexa" to keep alpha or "rgb" / "hsl" / "css". form associates the input with a <form> by id.
import { useState } from "react" ;
import { Form } from "react-aria-components" ;
import { Button } from "@/components/ui/button" ;
import { ColorField, ColorPicker } from "@/components/ui/color-picker" ;
export default function ColorPickerForm () {
const [ result , setResult ] = useState < string | null >( null );
return (
< Form
className = "flex w-full max-w-60 flex-col gap-4"
onSubmit = {( e ) => {
e. preventDefault ();
setResult (
JSON . stringify (Object. fromEntries ( new FormData (e.currentTarget))),
);
}}
>
< ColorField
label = "Background"
name = "background"
defaultValue = "#f8fafc"
isRequired
/>
< div className = "flex flex-wrap gap-2" >
< ColorPicker label = "Accent" name = "accent" defaultValue = "#db2777" />
< ColorPicker
label = "Overlay"
name = "overlay"
valueFormat = "hexa"
defaultValue = "#0f172a80"
/>
</ div >
< Button type = "submit" className = "self-start" >
Save theme
</ Button >
{result && (
< code className = "break-all rounded-md bg-muted px-2 py-1 text-xs" >
{result}
</ code >
)}
</ Form >
);
}
A brand color applied live to buttons and badges through the --tone and --tone-fg variables, with a readable foreground picked automatically.
Brand color Used for buttons, links and highlights.
#7C3AEDUpgrade plan SubscribeNew Accent text
import { BellIcon } from "lucide-react" ;
import { type CSSProperties, useState } from "react" ;
import { type Color, parseColor } from "react-aria-components" ;
import { Badge } from "@/components/ui/badge" ;
import { Button } from "@/components/ui/button" ;
import { ColorPicker } from "@/components/ui/color-picker" ;
/** Black or white, whichever reads better on the given color. */
function foregroundFor ( color : Color ) {
const rgb = color. toFormat ( "rgb" );
const [ r , g , b ] = ([ "red" , "green" , "blue" ] as const ). map (( c ) =>
rgb. getChannelValue (c),
);
return (r * 299 + g * 587 + b * 114 ) / 1000 > 150 ? "#0a0a0a" : "#ffffff" ;
}
export default function ColorPickerRecipeThemeEditor () {
const [ brand , setBrand ] = useState < Color >( parseColor ( "#7c3aed" ));
const tone = {
"--tone" : brand. toString ( "hex" ),
"--tone-fg" : foregroundFor (brand),
} as CSSProperties ;
return (
< div className = "flex w-full max-w-md flex-col gap-5 rounded-xl border bg-card p-5" >
< div className = "flex items-center justify-between gap-4" >
< div >
< h3 className = "font-semibold" >Brand color</ h3 >
< p className = "text-muted-foreground text-sm" >
Used for buttons, links and highlights.
</ p >
</ div >
< ColorPicker
label = {brand. toString ( "hex" ). toUpperCase ()}
value = {brand}
onChange = {setBrand}
swatches = {[ "#7c3aed" , "#2563eb" , "#0d9488" , "#ea580c" , "#e11d48" ]}
/>
</ div >
< div
style = {tone}
className = "flex flex-wrap items-center gap-3 rounded-lg border border-dashed p-4"
>
< Button style = {tone}>Upgrade plan</ Button >
< Button style = {tone} variant = "soft" >
< BellIcon /> Subscribe
</ Button >
< Badge style = {tone} variant = "soft" >
New
</ Badge >
< span className = "font-medium text-(--tone) text-sm" >Accent text</ span >
</ div >
</ div >
);
}
A name field and a palette of presets, with a live preview of the resulting label.
import { TagIcon } from "lucide-react" ;
import { useState } from "react" ;
import { type Color, parseColor } from "react-aria-components" ;
import { Button } from "@/components/ui/button" ;
import {
ColorSwatchPicker,
ColorSwatchPickerItem,
} from "@/components/ui/color-picker" ;
import { Label } from "@/components/ui/field" ;
import { TextField } from "@/components/ui/text-field" ;
const colors = [
"#64748b" ,
"#dc2626" ,
"#ea580c" ,
"#ca8a04" ,
"#16a34a" ,
"#0891b2" ,
"#2563eb" ,
"#7c3aed" ,
"#db2777" ,
];
export default function ColorPickerRecipeLabelEditor () {
const [ name , setName ] = useState ( "Needs design" );
const [ color , setColor ] = useState < Color >( parseColor ( "#7c3aed" ));
const hex = color. toString ( "hex" );
return (
< div className = "flex w-full max-w-xs flex-col gap-4 rounded-xl border bg-card p-5" >
< div className = "flex items-center gap-2 text-muted-foreground text-sm" >
< TagIcon className = "size-4" /> New label
</ div >
< TextField label = "Name" value = {name} onChange = {setName} />
< div className = "flex flex-col gap-2" >
< Label id = "label-color" >Color</ Label >
< ColorSwatchPicker
aria-labelledby = "label-color"
value = {color}
onChange = {setColor}
>
{colors. map (( c ) => (
< ColorSwatchPickerItem key = {c} color = {c} />
))}
</ ColorSwatchPicker >
</ div >
< div className = "flex items-center justify-between gap-3 border-t pt-4" >
< span
className = "inline-flex h-6 items-center gap-1.5 rounded-full border px-2.5 font-medium text-xs"
style = {{
color: hex,
borderColor: `${ hex }55` ,
backgroundColor: `${ hex }14` ,
}}
>
< span
className = "size-1.5 rounded-full"
style = {{ backgroundColor: hex }}
/>
{name || "Label" }
</ span >
< Button size = "sm" isDisabled = { ! name. trim ()}>
Create label
</ Button >
</ div >
</ div >
);
}
Two pickers and a Slider for the angle, producing copyable CSS.
background: linear-gradient(135deg, #F97316, #DB2777);
import { useState } from "react" ;
import { type Color, parseColor } from "react-aria-components" ;
import { ColorPicker } from "@/components/ui/color-picker" ;
import { Slider } from "@/components/ui/slider" ;
export default function ColorPickerRecipeGradient () {
const [ from , setFrom ] = useState < Color >( parseColor ( "#f97316" ));
const [ to , setTo ] = useState < Color >( parseColor ( "#db2777" ));
const [ angle , setAngle ] = useState ( 135 );
const css = `linear-gradient(${ angle }deg, ${ from . toString ( "hex" ) }, ${ to . toString ( "hex" ) })` ;
return (
< div className = "flex w-full max-w-sm flex-col gap-4" >
< div
className = "h-32 rounded-xl border"
style = {{ backgroundImage: css }}
role = "img"
aria-label = "Gradient preview"
/>
< div className = "flex flex-wrap gap-2" >
< ColorPicker label = "From" value = {from} onChange = {setFrom} />
< ColorPicker label = "To" value = {to} onChange = {setTo} />
</ div >
< Slider
label = "Angle"
value = {angle}
onChange = {setAngle}
maxValue = { 360 }
step = { 5 }
formatOptions = {{ style: "unit" , unit: "degree" }}
color = "neutral"
/>
< code className = "break-all rounded-md bg-muted px-2 py-1.5 text-xs" >
{ `background: ${ css };` }
</ code >
</ div >
);
}
The ColorPicker trigger is a button; the popover is a labelled dialog (named by label, then aria-label, or "Color picker") that traps focus, closes on Esc and returns focus to the trigger.
The default editor's preset list is labelled "Presets".
ColorArea exposes two visually hidden range inputs, one per channel, announced as a two-dimensional slider. Values are announced with the color's name, like "vibrant purple".
ColorSlider is a native range input announced with its channel name and value. Without a label it's labelled by the channel name; pass aria-label to override it.
ColorField without a visible label needs an aria-label (the default popover uses "Hex").
ColorSwatchPicker is a listbox: one tab stop, arrow keys to move, and each option announced by color name.
Don't rely on color alone to convey meaning in what users build with the picked color; check contrast when a color is used for text.
Key Action Tab Moves between the trigger, area, sliders, fields and swatches Enter / Space On the trigger: opens the popover. On a swatch: selects it. ← / → / ↑ / ↓ In the area: adjusts the x / y channel. In a slider: adjusts the channel. In the swatch picker: moves focus. Shift + arrowsIn the area or a slider: adjusts by a larger step Page Up / Page Down Adjusts the area's y channel, or a slider, by a larger step Home / End Adjusts the area's x channel by a larger step; sets a slider to its min / max ↑ / ↓ in a fieldSteps the value Esc Closes the popover
Part Attributes ColorAreadata-disabledColorSliderdata-disabled, data-orientationColorThumbdata-dragging, data-hovered, data-focused, data-focus-visible, data-disabledColorFielddata-disabled, data-invalid, data-readonly, data-required, data-channelColorSwatchPickerItemdata-selected, data-hovered, data-pressed, data-focused, data-focus-visible, data-disabled
The thumb grows while dragging or keyboard-focused (data-dragging:size-5.5, data-focus-visible:size-5.5).
data-slotElement color-area, color-slider, color-thumbArea, slider and their thumb color-fieldField root (input on the input itself) color-swatchEvery swatch, including the trigger's and each preset's color-swatch-picker, color-swatch-picker-itemPreset list and items label, description, field-errorField and slider text color-picker-triggerThe ColorPicker trigger button (it also has button's styles) popoverThe ColorPicker popover
ColorArea is size-48 by default; className="w-full" makes it fill the popover or its container. ColorSwatch is size-7; ColorSwatchPickerItem always renders a size-7 swatch. The popover is 13.5rem wide.
Every part's className accepts a function of its state, e.g. the swatch picker item's isSelected or the thumb's isDragging:
< ColorSwatchPickerItem
color = "#2563eb"
className = {({ isSelected }) => (isSelected ? "scale-110" : "" )}
/>
These are the props ColorPicker accepts. React Aria's ColorPicker is the state provider underneath.
Also accepts every prop of React Aria's ColorArea .
Also accepts every prop of React Aria's ColorSlider . Styled for horizontal sliders only.
Also accepts every prop of React Aria's ColorField .
See React Aria's ColorSwatchPicker and ColorSwatch .
Accepts React Aria's ColorThumb props (className, style, hover events). ColorArea and ColorSlider render one for you.
Slider — the same interaction for plain numbers.
Popover — the overlay the picker opens in.
Text Field — ColorField shares its input styles.