sdxc

Type to search, or start from one of these:

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/ui
import "@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.

  • ColorPicker
  • ColorPicker.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 via commandfor/command="toggle-popover" to ColorPicker.Dialog's id.
  • ColorPicker.DialogRenders ColorPicker's picking surface: a Popover, defaulting placement to bottom-start and role to "dialog", padding whatever picking controls a consumer composes as children in 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.

PropTypeDescription
color?ColorSemantic color role for the fallback field's focus ring. Read only when children is unset.
label?RemixNodeThe fallback field's caption, rendered through ColorField. Read only when children is unset.
description?RemixNodeSupporting copy beneath the fallback field. Read only when children is unset.
errorMessage?RemixNodeValidation message beneath the fallback field. Read only when children is unset.
format?ColorField.FormatNotation the fallback field's typed entry is constrained to. Read only when children is unset.
name?stringNative name submitted with an enclosing form, read only by the fallback field.
value?stringCurrent value, as a literal color string, for a fallback field a consumer tracks itself.
defaultValue?stringInitial value, as a literal color string, for a fallback field left to the platform's own uncontrolled state.
placeholder?stringPlaceholder copy shown while the fallback field is empty.
required?booleanMarks the fallback field required for its enclosing form.
disabled?booleanMarks the fallback field inert and excluded from the tab order.
readOnly?booleanMarks the fallback field's value fixed, while keeping it focusable and included in form submission.
autoComplete?stringNative autofill hint for the fallback field.
parts?PartsPropsPer-part styling for the fallback field's internally composed parts. Read only when children is unset.
children?RemixNodeThe 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

PropTypeDescription
valuestringThe 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.ShapeShape 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.