Color Picker
BetaBeta — the props contract is close to final, but small breaking changes can still land.Color input with picker UI, formats, presets, contrast helpers, and a gradient editor with draggable stops.
Examples
Inline panel
Render the editor in place with ColorPickerPanel when the color is the main task, like a theme or brand settings page.
Color wheel
The wheel sets hue by angle and saturation by distance from the center; the brightness strip completes the color.
Inline strips
Each strip edits one coordinate of the same color, so the parts fit a toolbar or inspector row without a popup.
Gradient editor
Drag stops along the track, pick each stop color with the same picker, and read back a CSS gradient.
Composition
Popup color editor
Area, Channels, and Swatches supply their default children when omitted. AreaContrast draws the WCAG threshold for a background over the area.
- <ColorPicker>
- <ColorPickerTrigger />
- <ColorPickerOutput />
- <ColorPickerContent>
- <ColorPickerArea>
- <ColorPickerAreaContrast />
- <ColorPickerAreaThumb />
- <ColorPickerEyeDropper />
- <ColorPickerHue />
- <ColorPickerAlpha />
- <ColorPickerFormatSelect />
- <ColorPickerInput />
- <ColorPickerChannels>
- <ColorPickerChannel />
- <ColorPickerContrast />
- <ColorPickerSwatches>
- <ColorPickerSwatch />
- <ColorPickerSwatchAdd />
- <ColorPickerArea>
Inline color wheel
- <ColorPicker>
- <ColorPickerPanel>
- <ColorPickerWheel />
- <ColorPickerBrightness />
- <ColorPickerInput />
- <ColorPickerPanel>
Inline strips
Every strip edits one coordinate of the same color, so parts sit in a toolbar or inspector row without a popup.
- <ColorPicker>
- <ColorPickerSwatch />
- <ColorPickerHue />
- <ColorPickerSaturation />
- <ColorPickerBrightness />
- <ColorPickerOutput />
Gradient stops
Track renders a stop for each entry; supply children to customize those stops. The value is structured; formatGradient turns it into CSS.
- <GradientEditor>
- <GradientEditorPreview />
- <GradientEditorTrack>
- <GradientEditorStop />
- <GradientEditorStopColor />
- <GradientEditorTypeSelect />
- <GradientEditorInterpolationSelect />
- <GradientEditorAngle />
- <GradientEditorStopAdd />
Installation
First install and activate one skin. Core deliberately contains no visual token defaults.
The Control UI source installs this primitive from src/registry/sources/control-ui/ui/color-picker.tsx.
npx shadcn@latest add https://control-ui.dev/r/color-picker.jsonDependencies
src/registry/hooks/use-color-area.tsHooksrc/registry/lib/color.tsSupportsrc/registry/lib/contrast.tsSupportsrc/registry/lib/gradient.tsUtilsrc/registry/sources/control-ui/ui/color-picker-area.tsxSupportsrc/registry/sources/control-ui/ui/color-picker-context.tsSupportsrc/registry/sources/control-ui/ui/color-picker-contrast.tsxSupportsrc/registry/sources/control-ui/ui/color-picker-sliders.tsxSupportsrc/registry/sources/control-ui/ui/color-picker-swatches.tsxSupportsrc/registry/sources/control-ui/ui/color-picker-values.tsxSupportsrc/registry/sources/control-ui/ui/gradient-editor-controls.tsxSupportsrc/registry/sources/control-ui/ui/gradient-editor.tsxSupportsrc/registry/sources/control-ui/surface-variants.tsSupportsrc/registry/sources/control-ui/control-variants.tsSupportbuttonPrimitiveInputinputPrimitiveNumber Fieldnumber-fieldPrimitiveSelectselectPrimitiveRaw code
Primary installed source
"use client";import { Popover as PopoverPrimitive } from "@base-ui/react/popover";import type { ComponentProps, CSSProperties, ReactNode } from "react";import type { OpenChangeEventDetails } from "@/components/control-ui/control-props";import type { ColorPickerKnobStyle } from "@/components/control-ui/knob-contracts/color-picker-knobs";import type { PopupKnobStyle } from "@/components/control-ui/knob-contracts/popup-knobs";import { cn } from "@/components/control-ui/lib/cn";import { controlEffectsAttribute } from "@/components/control-ui/skin";import { useSkin } from "@/components/control-ui/skin-provider";import { ColorPickerArea } from "@/components/control-ui/ui/color-picker-area";import { ColorPickerContext, type ColorPickerStateProps, useColorPicker, useColorState,} from "@/components/control-ui/ui/color-picker-context";import { ColorPickerAlpha, ColorPickerHue } from "@/components/control-ui/ui/color-picker-sliders";import { ColorPickerFormatSelect, ColorPickerInput } from "@/components/control-ui/ui/color-picker-values";import { useGradientEditor } from "@/components/control-ui/ui/gradient-editor";export type { ColorFormat } from "@/components/control-ui/lib/color";// biome-ignore lint/performance/noBarrelFile: Preserve the color picker install-facing API.export { ColorPickerArea, type ColorPickerAreaProps, ColorPickerAreaThumb, type ColorPickerAreaThumbProps, ColorPickerWheel, type ColorPickerWheelProps,} from "@/components/control-ui/ui/color-picker-area";export { ColorPickerAreaContrast, type ColorPickerAreaContrastProps, ColorPickerContrast, type ColorPickerContrastProps,} from "@/components/control-ui/ui/color-picker-contrast";export { ColorPickerAlpha, type ColorPickerAlphaProps, ColorPickerBrightness, type ColorPickerBrightnessProps, ColorPickerHue, type ColorPickerHueProps, ColorPickerSaturation, type ColorPickerSaturationProps,} from "@/components/control-ui/ui/color-picker-sliders";export { ColorPickerSwatch, ColorPickerSwatchAdd, type ColorPickerSwatchAddProps, ColorPickerSwatches, type ColorPickerSwatchesProps, type ColorPickerSwatchProps,} from "@/components/control-ui/ui/color-picker-swatches";export { ColorPickerChannel, type ColorPickerChannelProps, ColorPickerChannels, type ColorPickerChannelsProps, ColorPickerEyeDropper, type ColorPickerEyeDropperProps, ColorPickerFormatSelect, type ColorPickerFormatSelectProps, ColorPickerInput, type ColorPickerInputProps, ColorPickerOutput, type ColorPickerOutputProps,} from "@/components/control-ui/ui/color-picker-values";export { GradientEditor, GradientEditorPreview, type GradientEditorPreviewProps, type GradientEditorProps, GradientEditorStop, GradientEditorStopAdd, type GradientEditorStopAddProps, type GradientEditorStopProps, GradientEditorTrack, type GradientEditorTrackProps, type GradientInterpolation, type GradientStop, type GradientType, type GradientValue,} from "@/components/control-ui/ui/gradient-editor";export { GradientEditorAngle, type GradientEditorAngleProps, GradientEditorInterpolationSelect, type GradientEditorInterpolationSelectProps, GradientEditorTypeSelect, type GradientEditorTypeSelectProps,} from "@/components/control-ui/ui/gradient-editor-controls";export type ColorPickerProps = ColorPickerStateProps & { open?: boolean; defaultOpen?: boolean; onOpenChange?: (open: boolean, eventDetails: OpenChangeEventDetails) => void; children?: ReactNode;};export type ColorPickerTriggerProps = Omit<ComponentProps<"button">, "style"> & { style?: CSSProperties & ColorPickerKnobStyle };export type ColorPickerContentProps = Omit<ComponentProps<"div">, "style"> & { style?: CSSProperties & PopupKnobStyle } & { side?: "top" | "right" | "bottom" | "left"; align?: "start" | "center" | "end"; sideOffset?: number;};export type ColorPickerPanelProps = Omit<ComponentProps<"div">, "style"> & { style?: CSSProperties & PopupKnobStyle };export type GradientEditorStopColorProps = { children?: ReactNode };export function ColorPicker({ open, defaultOpen, onOpenChange, children, ...state }: ColorPickerProps) { const ctx = useColorState(state); return ( <ColorPickerContext.Provider value={ctx}> <PopoverPrimitive.Root open={open} defaultOpen={defaultOpen} onOpenChange={onOpenChange}> {children} </PopoverPrimitive.Root> </ColorPickerContext.Provider> );}export function ColorPickerTrigger({ className, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, ...props}: ColorPickerTriggerProps) { const { valueString, disabled } = useColorPicker(); return ( <PopoverPrimitive.Trigger data-control-ui="color-picker" data-control-family="color-picker" data-slot="trigger" data-disabled={disabled ? "true" : undefined} disabled={disabled} aria-label={ariaLabelledBy === undefined ? (ariaLabel ?? `Choose color (${valueString})`) : ariaLabel} aria-labelledby={ariaLabelledBy} className={cn("relative inline-flex shrink-0 cursor-pointer overflow-hidden disabled:cursor-not-allowed", className)} {...props} > <span data-control-ui="color-picker" data-control-family="color-picker" data-slot="trigger-checker" aria-hidden className="absolute inset-0" /> <span data-control-ui="color-picker" data-control-family="color-picker" data-slot="trigger-color" aria-hidden className="absolute inset-0" style={{ backgroundColor: valueString }} /> </PopoverPrimitive.Trigger> );}export function ColorPickerContent({ className, children, side = "bottom", align = "center", sideOffset = 6, ...props}: ColorPickerContentProps) { const skin = useSkin(); return ( <PopoverPrimitive.Portal> <PopoverPrimitive.Positioner data-control-ui="color-picker" data-popup-kind="color-picker" data-control-family="popup" data-slot="positioner" data-skin={skin.id} data-effects={controlEffectsAttribute(skin.effects)} side={side} align={align} sideOffset={sideOffset} className="z-(--z-popup)" > <PopoverPrimitive.Popup data-control-ui="color-picker" data-popup-kind="color-picker" data-control-family="popup" data-slot="content" data-surface="floating" data-popup-part="surface" className={cn("grid", className)} {...props} > {children} </PopoverPrimitive.Popup> </PopoverPrimitive.Positioner> </PopoverPrimitive.Portal> );}export function ColorPickerPanel({ className, children, ...props }: ColorPickerPanelProps) { return ( <div data-control-ui="color-picker" data-popup-kind="color-picker" data-control-family="popup" data-slot="panel" data-surface="floating" data-popup-part="surface" data-popup-static="" className={cn("grid", className)} {...props} > {children} </div> );}export function GradientEditorStopColor({ children }: GradientEditorStopColorProps) { const { selectedStop, setStopColor } = useGradientEditor(); if (!selectedStop) return null; return ( <ColorPicker value={selectedStop.color} onValueChange={(color) => setStopColor(selectedStop.id, color)} defaultFormat="hex"> {children ?? ( <> <ColorPickerTrigger /> <ColorPickerContent> <ColorPickerArea /> <ColorPickerHue /> <ColorPickerAlpha /> <div data-control-ui="gradient-editor" data-control-family="gradient-editor" data-slot="value-row" className="flex"> <ColorPickerFormatSelect /> <ColorPickerInput className="flex-1" /> </div> </ColorPickerContent> </> )} </ColorPicker> );}Knobs
Typed custom properties the recipe paints with. Set one on the root — style, a utility class, or a skin — and every slot inherits it.
--cui-color-picker-* · 7 knobsHow the cascade resolves--cui-color-picker-output-swatch-radius<length-percentage>+var(--radius-md)--cui-color-picker-swatch-radius<length-percentage>+var(--radius-md)--cui-color-picker-area-radius<length-percentage>+var(--radius-field)--cui-color-picker-trigger-radius<length-percentage>+var(--radius-control)--cui-color-picker-trigger-shadow*inset 0 0 0 var(--control-rim-width) var(--control-rim)--cui-color-picker-slider-thumb-radius<length-percentage>9999px--cui-color-picker-slider-thumb-background<color>transparent--cui-gradient-editor-* · 10 knobsHow the cascade resolves--cui-gradient-editor-preview-radius<length-percentage>var(--radius-field)--cui-gradient-editor-preview-ring-color<color>oklch(from var(--border) l c h / 0.6)--cui-gradient-editor-track-radius<length-percentage>9999px--cui-gradient-editor-stop-radius<length-percentage>9999px--cui-gradient-editor-stop-border-color<color>oklch(1 0 0)--cui-gradient-editor-stop-border-width<length>2px--cui-gradient-editor-stop-shadow*0 0 0 1px oklch(from var(--foreground) l c h / 0.4)--cui-gradient-editor-add-radius<length-percentage>9999px--cui-gradient-editor-add-border-color<color>var(--border)--cui-gradient-editor-add-border-width<length>var(--control-rim-width)--cui-popup-* · 44 knobsHow the cascade resolves--cui-popup-title-foreground<color>var(--cui-popup-foreground)--cui-popup-title-font-size<length-percentage>var(--text-heading-3)--cui-popup-title-font-weight<number>var(--font-weight-semibold)--cui-popup-title-line-height<number>var(--text-heading-3--line-height)--cui-popup-title-letter-spacing<length>-0.025em--cui-popup-description-foreground<color>var(--muted-foreground)--cui-popup-close-inset<length-percentage>calc(var(--spacing) * 3)--cui-popup-item-inset<length-percentage>calc(var(--spacing) * 8)--cui-popup-label-padding-block<length-percentage>var(--cui-popup-item-padding-block)--cui-popup-label-padding-inline<length-percentage>var(--cui-popup-item-padding-inline)--cui-popup-label-font-size<length-percentage>var(--text-micro)--cui-popup-label-font-weight<number>var(--font-weight-medium)--cui-popup-label-letter-spacing<length>0.08em--cui-popup-label-text-transformnone | uppercase | lowercase | capitalizeuppercase--cui-popup-separator-inset<length-percentage>calc(var(--popover-padding) * -1)--cui-popup-shortcut-font-size<length-percentage>var(--text-body)--cui-popup-shortcut-letter-spacing<length>0.02em--cui-popup-status-success-foreground<color>var(--success-text)--cui-popup-status-warning-foreground<color>var(--warning-text)--cui-popup-status-info-foreground<color>var(--info-text)--cui-popup-status-error-foreground<color>var(--destructive)--cui-popup-radius<length-percentage>var(--radius-popover)--cui-popup-background<color>var(--popover)--cui-popup-foreground<color>var(--popover-foreground)--cui-popup-border-color<color>var(--control-rim)--cui-popup-border-width<length>var(--control-rim-width)--cui-popup-shadow*var(--shadow-pop)--cui-popup-backdrop-filter*blur(var(--backdrop-blur-popover))--cui-popup-backdrop-background<color>oklch(from var(--foreground) l c h / var(--overlay-opacity))--cui-popup-backdrop-blur<length>var(--backdrop-blur-overlay)--cui-popup-item-radius<length-percentage>var(--radius-popup-item-fit)--cui-popup-item-font-size<length-percentage>var(--text-body)--cui-popup-item-icon-size<length-percentage>1rem--cui-popup-item-padding-block<length-percentage>calc(var(--spacing) * 1)--cui-popup-item-padding-inline<length-percentage>calc(var(--padding-x) * 0.5)--cui-popup-item-min-height<length-percentage>max(var(--target-min), var(--control-h-xs))--cui-popup-item-highlight-shadow*inset 0 0 0 1px oklch(from var(--foreground) l c h / 0.35)--cui-popup-item-foreground<color>var(--foreground)--cui-popup-item-highlight-background<color>var(--hover-fill)--cui-popup-item-highlight-foreground<color>var(--cui-popup-item-foreground)--cui-popup-item-highlight-muted-foreground<color>var(--muted-foreground)--cui-popup-item-disabled-opacity<number>var(--disabled-opacity)--cui-popup-separator-color<color>var(--border)--cui-popup-shortcut-foreground<color>var(--muted-foreground)