Color Picker
Form-first color field with a swatch trigger, React Aria panel, CSS string values, and a headless hook.
Pick one color for a form field — brand accent, highlight, chart series — and store it as a CSS string. Opaque colors serialize as lowercase #rrggbb; translucent colors as rgba(r, g, b, a). The product is useColorPicker + ColorPickerPanel; use ColorPicker when the color is the field value (compact swatch trigger under the label). Built-in chrome strings use I18nProvider (registry item fui-i18n).
When To Use
- Form fields — theme accent, label color, calendar event color, or any single CSS color on create/edit screens.
- Opaque-only tokens — set
disabledAlphawhen the wire format must stay#rrggbb(no alpha slider). - Preset brands — pass
presetsfor known brand / neutral swatches above the freeform panel. - Custom chrome — compose
useColorPickerwith your own trigger and the same panel (toolbar chip, inline swatch).
For Formily forms, use kind="color" on Form (Formily) — see the Formily example below.
Do not use this for gradients, design-token browsers, or multi-stop palettes — those are out of scope for v1.
Interactions
| Event | Default behavior |
|---|---|
| Trigger click | Open panel (Popover) |
| Drag area / sliders / type fields | onChange(css) while editing |
| Pointer up / field commit | onChangeComplete(css) when provided |
| Format toggle (hex / rgb / hsb) | Panel-only display; wire value stays a CSS string |
| Preset swatch click | Jump to that color |
| EyeDropper (when available) | Sample from the screen; commit on pick |
Clear (when allowClear) | onChange(undefined) |
| Escape / outside click | Close panel; onBlur on field container |
Installing
pnpm dlx shadcn@latest add https://ui.isaacfei.com/r/color-picker.jsonnpx shadcn@latest add https://ui.isaacfei.com/r/color-picker.jsonyarn dlx shadcn@latest add https://ui.isaacfei.com/r/color-picker.jsonbun x shadcn@latest add https://ui.isaacfei.com/r/color-picker.jsonOr with a namespace: npx shadcn@latest add @f-ui/color-picker.
The CLI pulls button, label, popover, toggle-group, tooltip, react-aria-components, lucide-react, and the fui-i18n bundle.
Usage
import { ColorPicker } from "@/components/f-ui/color-picker/color-picker";
<ColorPicker
label="Brand color"
value={color}
onChange={setColor}
/>;Examples
Basic
Default form field: label + compact swatch chip (Ant / Element shape — not a full-width Input). Empty state shows a blank swatch — helper copy belongs in description, not inside the trigger.
Stored as a CSS color string (#rrggbb or rgba).
Field value: —
"use client";
import { useState } from "react";
import { ColorPicker } from "@/components/f-ui/color-picker/color-picker";
export function ColorPickerDemo() {
const [color, setColor] = useState<string | null>(null);
return (
<div className="max-w-sm space-y-3">
<ColorPicker
label="Brand color"
description="Stored as a CSS color string (#rrggbb or rgba)."
value={color}
onChange={(css) => setColor(css ?? null)}
/>
<p className="text-muted-foreground text-xs">
Field value:{" "}
<span className="text-foreground font-medium font-mono tabular-nums">
{color ?? "—"}
</span>
</p>
</div>
);
}Presets
presets render labeled swatch groups in the panel. Click a swatch to jump to that color without dragging the area.
Click a swatch under Presets to jump to a known brand color.
Field value: #1677ff
"use client";
import { useState } from "react";
import { ColorPicker } from "@/components/f-ui/color-picker/color-picker";
const PRESETS = [
{
label: "Brand",
colors: ["#1677ff", "#52c41a", "#faad14", "#ff4d4f", "#722ed1"],
defaultOpen: true,
},
{
label: "Neutral",
colors: ["#000000", "#595959", "#8c8c8c", "#d9d9d9", "#ffffff"],
},
];
export function ColorPickerPresetsDemo() {
const [color, setColor] = useState<string | null>("#1677ff");
return (
<div className="max-w-sm space-y-3">
<ColorPicker
label="Theme accent"
description="Click a swatch under Presets to jump to a known brand color."
value={color}
presets={PRESETS}
onChange={(css) => setColor(css ?? null)}
/>
<p className="text-muted-foreground text-xs">
Field value:{" "}
<span className="text-foreground font-medium font-mono tabular-nums">
{color ?? "—"}
</span>
</p>
</div>
);
}Disabled Alpha
disabledAlpha hides the alpha slider and always writes lowercase #rrggbb, even if the source color had transparency.
disabledAlpha hides the alpha slider and always serializes #rrggbb.
Field value: #1677ff
"use client";
import { useState } from "react";
import { ColorPicker } from "@/components/f-ui/color-picker/color-picker";
export function ColorPickerDisabledAlphaDemo() {
const [color, setColor] = useState<string | null>("#1677ff");
return (
<div className="max-w-sm space-y-3">
<ColorPicker
label="Opaque fill"
description="disabledAlpha hides the alpha slider and always serializes #rrggbb."
value={color}
disabledAlpha
onChange={(css) => setColor(css ?? null)}
/>
<p className="text-muted-foreground text-xs">
Field value:{" "}
<span className="text-foreground font-medium font-mono tabular-nums">
{color ?? "—"}
</span>
</p>
</div>
);
}Show Text
showText prints the CSS string beside the swatch on the same content-sized chip (still a panel trigger — not a typeable field). Pass a render function for custom formatting.
showText keeps a content-sized chip (swatch + CSS) — not a full-width Input.
Field value: #52c41a
"use client";
import { useState } from "react";
import { ColorPicker } from "@/components/f-ui/color-picker/color-picker";
export function ColorPickerShowTextDemo() {
const [color, setColor] = useState<string | null>("#52c41a");
return (
<div className="max-w-sm space-y-3">
<ColorPicker
label="Accent"
description="showText keeps a content-sized chip (swatch + CSS) — not a full-width Input."
value={color}
showText
onChange={(css) => setColor(css ?? null)}
/>
<p className="text-muted-foreground text-xs">
Field value:{" "}
<span className="text-foreground font-medium font-mono tabular-nums">
{color ?? "—"}
</span>
</p>
</div>
);
}Allow Clear
Hover the trigger to reveal clear when a value is set. Clear commits undefined / null at the Formily boundary. With showText, a cleared field drops the CSS caption (swatch-only) — it does not show “Transparent” (that Ant label is for transparent-color clear, not null; see research).
Hover the trigger to reveal clear when a value is set.
Field value: #722ed1
"use client";
import { useState } from "react";
import { ColorPicker } from "@/components/f-ui/color-picker/color-picker";
export function ColorPickerAllowClearDemo() {
const [color, setColor] = useState<string | null>("#722ed1");
return (
<div className="max-w-sm space-y-3">
<ColorPicker
label="Highlight"
description="Hover the trigger to reveal clear when a value is set."
value={color}
allowClear
showText
onChange={(css) => setColor(css ?? null)}
/>
<p className="text-muted-foreground text-xs">
Field value:{" "}
<span className="text-foreground font-medium font-mono tabular-nums">
{color ?? "—"}
</span>
</p>
</div>
);
}Formily Kind
kind="color" connects the same control through Formily. Pass picker props via componentProps.
"use client";
import { useMemo, useState } from "react";
import { toast } from "sonner";
import { Form } from "@/components/f-ui/formily/form";
import { FormActions } from "@/components/f-ui/formily/form-actions";
import { FormField } from "@/components/f-ui/formily/form-field";
import { createForm } from "@/components/f-ui/formily/internals/create-form";
import { Button } from "@/components/ui/button";
interface ThemeValues {
brandColor: string | null;
}
export function ColorPickerFormilyDemo() {
const form = useMemo(
() =>
createForm<ThemeValues>({
initialValues: { brandColor: "#1677ff" },
}),
[],
);
const [submitted, setSubmitted] = useState<ThemeValues | null>(null);
return (
<div className="w-full max-w-sm space-y-4">
<Form
form={form}
onSubmit={(values) => {
setSubmitted({ ...values });
toast.success("Submitted successfully");
}}
>
<FormField
name="brandColor"
label="Brand color"
kind="color"
description="Formily kind=color commits a CSS string (or null when cleared)."
componentProps={{
allowClear: true,
showText: true,
presets: [
{
label: "Brand",
colors: ["#1677ff", "#52c41a", "#faad14", "#f5222d", "#722ed1"],
},
],
}}
/>
<FormActions>
<Button type="submit">
Save theme
</Button>
</FormActions>
</Form>
{submitted ? (
<pre className="bg-muted text-muted-foreground overflow-auto rounded-md p-3 text-xs">
<code>{JSON.stringify(submitted, null, 2)}</code>
</pre>
) : null}
</div>
);
}Headless Usage
useColorPicker is the view-model: open state, CSS value, format, presets, EyeDropper, and triggerProps. Your field chrome can be plain HTML — the demo uses an unstyled <button> and <label>, not ColorPickerTrigger. Wire panel props into ColorPickerPanel inside ColorPickerPopover.
value: —
"use client";
import { useState } from "react";
import {
ColorPickerPanel,
ColorPickerPopover,
useColorPicker,
} from "@/components/f-ui/color-picker/color-picker";
import { Popover, PopoverTrigger } from "@/components/ui/popover";
export function ColorPickerHeadlessDemo() {
const [color, setColor] = useState<string | null>(null);
const picker = useColorPicker({
value: color,
onChange: (css) => setColor(css ?? null),
});
const { openLabel } = picker.triggerProps;
return (
<Popover modal={false} open={picker.open} onOpenChange={picker.onOpenChange}>
<div>
<label htmlFor="headless-color-picker">Brand color</label>
<div>
<PopoverTrigger asChild>
<button
id="headless-color-picker"
type="button"
aria-label={openLabel}
>
{color ?? "Pick color"}
</button>
</PopoverTrigger>
</div>
<p>value: {color ?? "—"}</p>
</div>
<ColorPickerPopover align="start">
<ColorPickerPanel
color={picker.color}
setColorFromRac={picker.setColorFromRac}
format={picker.format}
setFormat={picker.setFormat}
disabledAlpha={picker.disabledAlpha}
presets={picker.presets}
eyeDropperAvailable={picker.eyeDropperAvailable}
pickFromEyeDropper={picker.pickFromEyeDropper}
t={picker.t}
/>
</ColorPickerPopover>
</Popover>
);
}Composition
ColorPicker (form field)
├── Label (optional)
├── Popover
│ ├── PopoverTrigger → ColorPickerTrigger (swatch ± text ± clear)
│ └── ColorPickerPopover → ColorPickerPanel
│ ├── ColorPickerArea
│ ├── ColorPickerSliders (hue ± alpha)
│ ├── ColorPickerFields (hex / rgb / hsb)
│ ├── ColorPickerPresets (optional)
│ └── ColorPickerEyedropper (when available)
├── Description (optional)
└── Error message (optional)
Headless
├── Popover + PopoverTrigger asChild → your trigger
└── ColorPickerPopover → ColorPickerPanel (same panel tree)API Reference
Props
| Prop | Type | Default |
|---|---|---|
value | string | null | — |
defaultValue | string | null | — |
onChange | (css: string | undefined) => void | — |
onChangeComplete | (css: string) => void | — |
onBlur | () => void | — |
disabledAlpha | boolean | false |
format | "hex" | "rgb" | "hsb" | — |
defaultFormat | "hex" | "rgb" | "hsb" | "hex" |
onFormatChange | (format) => void | — |
presets | ColorPickerPreset[] | — |
allowClear | boolean | false |
showText | boolean | ((css: string) => ReactNode) | false |
enableEyeDropper | boolean | true |
disabled | boolean | false |
isInvalid | boolean | false |
open | boolean | — |
onOpenChange | (open: boolean) => void | — |
locale | string | i18n/provider locale |
label | ReactNode | — |
description | ReactNode | — |
errorMessage | ReactNode | — |
className | string | — |
classNames | Partial<Record<ColorPickerSlot, string>> | — |
popover | ColorPickerPopoverProps | — |
t | ColorPickerTranslateFn | built-in/provider |
Slots
| Slot | Element |
|---|---|
root | Outer wrapper |
label | Field label |
trigger | Swatch trigger button |
swatch | Color swatch inside the trigger |
text | Optional CSS text beside the swatch |
popover | Popover panel |
panel | Color panel root |
area | Saturation / brightness area |
slider | Hue slider |
alpha | Alpha slider |
field | Channel / hex fields |
format | Format toggle group |
presets | Preset swatch groups |
eyedropper | EyeDropper button |
clear | Clear control on the trigger |
description | Description text |
errorMessage | Error text |
Hook — useColorPicker(options)
| Option | Type |
|---|---|
value | string | null |
defaultValue | string | null |
onChange | (css: string | undefined) => void |
onChangeComplete | (css: string) => void |
onBlur | () => void |
disabledAlpha | boolean |
format / defaultFormat / onFormatChange | panel format controls |
presets | ColorPickerPreset[] |
allowClear | boolean |
showText | boolean | ((css: string) => ReactNode) |
enableEyeDropper | boolean |
disabled | boolean |
isInvalid | boolean |
open / onOpenChange | controlled popover |
locale | string |
t | ColorPickerTranslateFn |
| Return | Type |
|---|---|
open / onOpenChange | popover open state |
color | parsed RAC Color or null |
cssValue | serialized CSS string or null |
format / setFormat | panel format |
disabledAlpha | alpha policy |
presets | preset groups |
eyeDropperAvailable | EyeDropper gated availability |
t | resolved translator |
setColorFromRac | (color, complete?) => void |
onClear | clear committed value |
pickFromEyeDropper | async screen sample |
triggerProps | spread on ColorPickerTrigger |