ColorPicker
A labeled color field composing ColorField's plain fallback with an optional ColorPicker.Group trigger row and ColorPicker.Dialog picking surface swapped in via children.
View code
let { hue, saturation, brightness } = OPENING_COLOR;
let alpha = 1;
let hex = formatHex({ ...hsvToRgb({ h: hue, s: saturation, v: brightness }), a: alpha });
// channelSync() reports every channel together rather than one at a time, so the
// sliders and the area stay one color rather than two views of different ones.
function moveChannel(event: ColorChannelChangeEvent) {
if (typeof event.values.saturation === "number") saturation = event.values.saturation;
if (typeof event.values.alpha === "number") alpha = event.values.alpha;
void handle.update();
}
<ColorPicker>
<Label htmlFor="preview-brand-color">Brand color</Label>
<ColorPicker.Group>
<Input
id="preview-brand-color"
type="text"
name="brandColor"
value={hex}
mix={[font("mono")]}
/>
<ColorPicker.Trigger
commandfor="preview-brand-color-panel"
command="toggle-popover"
aria-label="Open the color picker"
value={hex}
/>
</ColorPicker.Group>
<ColorPicker.Dialog id="preview-brand-color-panel" mix={[is("16rem")]}>
<ColorArea
aria-label="Saturation and brightness"
hue={hue}
saturation={saturation}
value={brightness}
style={{ "--ui-color-area-size": "12rem" }}
mix={[
colorAreaDrag(),
on<HTMLDivElement, "ui:color-area-change">("ui:color-area-change", (event) => {
saturation = event.x;
brightness = event.y;
void handle.update();
}),
]}
>
<ColorArea.SaturationThumb data-color-area-axis="x" aria-label="Saturation" />
<ColorArea.ValueThumb data-color-area-axis="y" aria-label="Brightness" />
</ColorArea>
<div mix={[hstack({ gap: 3, align: "center" })]}>
<ColorWheel
aria-label="Hue"
value={hue}
mix={[
is("5rem"),
bs("5rem"),
colorWheelDrag(),
on<HTMLDivElement, "ui:color-wheel-change">("ui:color-wheel-change", (event) => {
hue = event.hue;
void handle.update();
}),
]}
/>
<div
mix={[
vstack({ gap: 2, align: "stretch" }),
basis(0),
grow(),
minIs(0),
channelSync(),
on<HTMLDivElement, "ui:color-channel-change">("ui:color-channel-change", moveChannel),
]}
>
<ColorSlider channel="saturation" value={saturation}>
<ColorSlider.Track hue={hue}>
<ColorSlider.Thumb data-channel="saturation" aria-label="Saturation" />
</ColorSlider.Track>
</ColorSlider>
<ColorSlider channel="alpha" value={alpha}>
<ColorSlider.Track hue={hue}>
<ColorSlider.Thumb data-channel="alpha" aria-label="Opacity" />
</ColorSlider.Track>
</ColorSlider>
</div>
</div>
<Separator />
<ColorSwatchPicker
aria-label="Saved colors"
name="brandColorPreset"
mix={[on<HTMLDivElement, "change">("change", choosePreset)]}
>
{PRESETS.map((preset) => (
<ColorSwatchPicker.Swatch
key={preset.value}
value={preset.value}
aria-label={preset.label}
/>
))}
</ColorSwatchPicker>
<span mix={[font("mono"), text("xs"), fg("neutral")]}>{hex}</span>
</ColorPicker.Dialog>
</ColorPicker>Installation
An ordinary dependency: install it, import the theme once, and import the component where it is used.
npm add @sdxc/uiimport "@sdxc/ui/theme.css";Usage
import { ColorPicker } from "@sdxc/ui";Renders ColorPicker's root: ColorField's plain fallback
when children is unset, or the trigger-and-panel layout composed from
ColorPicker.Group and ColorPicker.Dialog otherwise.
Composition
The parts the component publishes, each a static property of the host.
ColorPickerColorPicker.GroupRenders ColorPicker's control row: a plain flex host laying the field's own control and ColorPicker.Trigger side by side, with a keyboard focus ring on the whole row the moment focus lands anywhere inside.ColorPicker.TriggerRenders ColorPicker's trailing trigger: a<button>housing a ColorSwatch that fills it as a live preview, wired viacommandfor/command="toggle-popover"to ColorPicker.Dialog'sid.ColorPicker.DialogRenders ColorPicker's picking surface: a Popover, defaultingplacementto bottom-start androleto"dialog", padding whatever picking controls a consumer composes aschildrenin a single column.
Examples
<ColorPicker>
<Label htmlFor="brandColor">{t("form.brandColor.label")}</Label>
<ColorPicker.Group>
<Input id="brandColor" type="text" name="brandColor" defaultValue="#3b82f6" />
<ColorPicker.Trigger
commandfor="brandColor-panel"
command="toggle-popover"
aria-label={t("form.brandColor.toggle")}
value="#3b82f6"
/>
</ColorPicker.Group>
<ColorPicker.Dialog id="brandColor-panel">
{...the picking surface — see ColorPicker.Dialog's own doc comment}
</ColorPicker.Dialog>
</ColorPicker>Props
Read from the component's own types, so every prop, every default and every allowed value is listed.
| Prop | Type | Description |
|---|---|---|
color? | Color | Semantic color role for the fallback field's focus ring. Read only when children is unset. |
label? | RemixNode | The fallback field's caption, rendered through ColorField. Read only when children is unset. |
description? | RemixNode | Supporting copy beneath the fallback field. Read only when children is unset. |
errorMessage? | RemixNode | Validation message beneath the fallback field. Read only when children is unset. |
format? | ColorField.Format | Notation the fallback field's typed entry is constrained to. Read only when children is unset. |
name? | string | Native name submitted with an enclosing form, read only by the fallback field. |
value? | string | Current value, as a literal color string, for a fallback field a consumer tracks itself. |
defaultValue? | string | Initial value, as a literal color string, for a fallback field left to the platform's own uncontrolled state. |
placeholder? | string | Placeholder copy shown while the fallback field is empty. |
required? | boolean | Marks the fallback field required for its enclosing form. |
disabled? | boolean | Marks the fallback field inert and excluded from the tab order. |
readOnly? | boolean | Marks the fallback field's value fixed, while keeping it focusable and included in form submission. |
autoComplete? | string | Native autofill hint for the fallback field. |
parts? | PartsProps | Per-part styling for the fallback field's internally composed parts. Read only when children is unset. |
children? | RemixNode | The trigger-and-panel layout — typically a Label, ColorPicker.Group, and ColorPicker.Dialog — rendered in place of ColorField's plain fallback. Unset renders that fallback. |
Also accepts everything in Omit<TagProps<"div">, "children">.
ColorPicker.Group
Also accepts everything in TagProps<"div">.
ColorPicker.Trigger
| Prop | Type | Description |
|---|---|---|
value | string | The color this trigger previews, already resolved to a literal CSS color value — the same value its inner ColorSwatch paints. Required, since a trigger with nothing to preview has no reason to render. |
shape? | ColorSwatch.Shape | Shape variant for the inner ColorSwatch, mirrored onto the button host so its own corners match. Defaults to DEFAULT_TRIGGER_SHAPE. |
Also accepts everything in Omit<TagProps<"button">, "children">.
ColorPicker.Dialog
Also accepts everything in Popover.Props.