f-ui
Components

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 disabledAlpha when the wire format must stay #rrggbb (no alpha slider).
  • Preset brands — pass presets for known brand / neutral swatches above the freeform panel.
  • Custom chrome — compose useColorPicker with 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

EventDefault behavior
Trigger clickOpen panel (Popover)
Drag area / sliders / type fieldsonChange(css) while editing
Pointer up / field commitonChangeComplete(css) when provided
Format toggle (hex / rgb / hsb)Panel-only display; wire value stays a CSS string
Preset swatch clickJump to that color
EyeDropper (when available)Sample from the screen; commit on pick
Clear (when allowClear)onChange(undefined)
Escape / outside clickClose panel; onBlur on field container

Installing

pnpm dlx shadcn@latest add https://ui.isaacfei.com/r/color-picker.json
npx shadcn@latest add https://ui.isaacfei.com/r/color-picker.json
yarn dlx shadcn@latest add https://ui.isaacfei.com/r/color-picker.json
bun x shadcn@latest add https://ui.isaacfei.com/r/color-picker.json

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

Formily kind=color commits a CSS string (or null when cleared).

"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

PropTypeDefault
valuestring | null
defaultValuestring | null
onChange(css: string | undefined) => void
onChangeComplete(css: string) => void
onBlur() => void
disabledAlphabooleanfalse
format"hex" | "rgb" | "hsb"
defaultFormat"hex" | "rgb" | "hsb""hex"
onFormatChange(format) => void
presetsColorPickerPreset[]
allowClearbooleanfalse
showTextboolean | ((css: string) => ReactNode)false
enableEyeDropperbooleantrue
disabledbooleanfalse
isInvalidbooleanfalse
openboolean
onOpenChange(open: boolean) => void
localestringi18n/provider locale
labelReactNode
descriptionReactNode
errorMessageReactNode
classNamestring
classNamesPartial<Record<ColorPickerSlot, string>>
popoverColorPickerPopoverProps
tColorPickerTranslateFnbuilt-in/provider

Slots

SlotElement
rootOuter wrapper
labelField label
triggerSwatch trigger button
swatchColor swatch inside the trigger
textOptional CSS text beside the swatch
popoverPopover panel
panelColor panel root
areaSaturation / brightness area
sliderHue slider
alphaAlpha slider
fieldChannel / hex fields
formatFormat toggle group
presetsPreset swatch groups
eyedropperEyeDropper button
clearClear control on the trigger
descriptionDescription text
errorMessageError text

Hook — useColorPicker(options)

OptionType
valuestring | null
defaultValuestring | null
onChange(css: string | undefined) => void
onChangeComplete(css: string) => void
onBlur() => void
disabledAlphaboolean
format / defaultFormat / onFormatChangepanel format controls
presetsColorPickerPreset[]
allowClearboolean
showTextboolean | ((css: string) => ReactNode)
enableEyeDropperboolean
disabledboolean
isInvalidboolean
open / onOpenChangecontrolled popover
localestring
tColorPickerTranslateFn
ReturnType
open / onOpenChangepopover open state
colorparsed RAC Color or null
cssValueserialized CSS string or null
format / setFormatpanel format
disabledAlphaalpha policy
presetspreset groups
eyeDropperAvailableEyeDropper gated availability
tresolved translator
setColorFromRac(color, complete?) => void
onClearclear committed value
pickFromEyeDropperasync screen sample
triggerPropsspread on ColorPickerTrigger

On this page