GradientEditor
6 partsThe headless root of a gradient-stop editor. It owns the list of color stops
(controlled via v-model or uncontrolled via defaultValue), the gradient
type ('linear' / 'radial'), and the linear angle, and exposes a shared
context so GradientEditorTrack, GradientEditorStops, GradientEditorStop,
GradientEditorAngle, and GradientEditorColorEditor stay in sync.
Each stop is { id, position, color } with position a fraction in [0, 1]
and color any CSS color string. The root keeps the stops sorted (stable
tie-break at identical positions), drags/keys a stop with snapping and either
neighbour-clamp (reorder: false) or cross-and-re-sort (reorder: true),
adds a stop on track clicks (color interpolated from neighbours), and never
removes below minStops. It also derives a cssGradient string for previews.
Per-stop color editing is delegated to a ColorField (compose
GradientEditorColorEditor, or mount one yourself). Reach for it whenever a
user should design a multi-stop gradient (CSS backgrounds, color ramps,
heatmap scales).
Demo
Anatomy
Import the parts and compose them. Each part forwards attributes to its underlying element.
import {
GradientEditorRoot,
GradientEditorAngle,
GradientEditorColorEditor,
GradientEditorStop,
GradientEditorStops,
GradientEditorTrack,
} from '@robonen/primitives/canvas/gradient-editor';
<GradientEditorRoot>
<GradientEditorAngle />
<GradientEditorColorEditor />
<GradientEditorStop />
<GradientEditorStops />
<GradientEditorTrack />
</GradientEditorRoot>API Reference
GradientEditorRoot
RootThe headless root of a gradient-stop editor. It owns the list of color stops (controlled via `v-model` or uncontrolled via `defaultValue`), the gradient `type` (`'linear'` / `'radial'`), and the linear `angle`, and exposes a shared context so `GradientEditorTrack`, `GradientEditorStops`, `GradientEditorStop`, `GradientEditorAngle`, and `GradientEditorColorEditor` stay in sync. Each stop is `{ id, position, color }` with `position` a fraction in `[0, 1]` and `color` any CSS color string. The root keeps the stops sorted (stable tie-break at identical positions), drags/keys a stop with snapping and either neighbour-clamp (`reorder: false`) or cross-and-re-sort (`reorder: true`), adds a stop on track clicks (color interpolated from neighbours), and never removes below `minStops`. It also derives a `cssGradient` string for previews. Per-stop color editing is delegated to a `ColorField` (compose `GradientEditorColorEditor`, or mount one yourself). Reach for it whenever a user should design a multi-stop gradient (CSS backgrounds, color ramps, heatmap scales).
| Prop | Type | Description |
|---|---|---|
defaultValue? | GradientStop[] | Uncontrolled initial stops. |
type? | GradientType | Gradient type. |
defaultAngle? | number | Uncontrolled initial linear angle in degrees. |
defaultSelectedId? | string | null | Uncontrolled initial selected stop id. |
minStops? | number | Minimum number of stops; removal is blocked at this floor. |
reorder? | boolean | Whether dragging / nudging a stop past a neighbour re-sorts the list
(true, each id keeps its color) or clamps to the neighbour so ids never
cross (false). |
step? | number | Keyboard step (fraction) for Arrow nudges. |
largeStep? | number | Large keyboard step (Shift+Arrow / Page keys). |
snapStep? | number | Optional grid snap increment (fraction) applied to drags and keyboard
moves. undefined disables grid snapping. |
disabled? | boolean | Disable all interaction. |
dir? | GradientEditorDirection | Writing direction. When omitted it is inherited from the nearest
ConfigProvider (falling back to 'ltr'); an explicit value wins. |
valueText? | GradientEditorValueText | Optional per-stop aria-valuetext formatter. |
modelValue? | GradientStop[] | null | Two-way bound value (v-model). |
angle? | number | null | Two-way bound value (v-model:angle). |
selectedId? | string | null | Two-way bound value (v-model:selectedId). |
| Event | Payload |
|---|---|
update:modelValue | [value: GradientStop[] | null] |
update:angle | [value: number | null] |
update:selectedId | [value: string | null] |
GradientEditorAngle
AngleOptional angle control for the linear gradient, wrapping an `AngleDialRoot` + `AngleDialThumb` bound to the root's `angle` model. It renders nothing when the gradient `type` is `'radial'` (the angle is meaningless there). Compose the dial's slot to position the thumb, or pass your own children via the default slot (which receives `{ angle }`).
| Prop | Type | Description |
|---|---|---|
min? | number | Min angle in degrees. |
max? | number | Max angle in degrees. |
step? | number | Step granularity in degrees. |
largeStep? | number | Large step (Page keys / Shift+Arrow) in degrees. |
GradientEditorColorEditor
ColorEditorOptional color editor for the selected stop. It mounts a `ColorFieldRoot` (from `../color-field`) whose `v-model` is bridged to the selected stop's `color`, so editing the color in any `ColorField` part (area, sliders, input, swatch) writes straight back to the stop via the root's `updateStop`. The `ColorField` parts are exposed through the default slot — drop in `ColorArea`, `HueSlider`, `ColorFieldInput`, etc. Renders nothing when no stop is selected (the default slot is suppressed). Consumers who want a different color UI can compose a `ColorField` themselves and bind it to `ctx.selectedId` / `ctx.updateStop` the same way.
| Prop | Type | Description |
|---|---|---|
format? | ColorFormat | Serialization format written back to the stop's color. |
GradientEditorStop
StopA single draggable color stop, rendered as `role="slider"`. It positions itself along the track by its `position` (`left: position * 100%`), carries the full ARIA value attributes (`aria-valuemin=0`, `aria-valuemax=1`, `aria-valuenow=position`, and an `aria-valuetext` of `` `<color> at <pct>%` `` — color is NEVER surfaced alone, WCAG 1.4.1), and reflects selection via `aria-selected` + `data-selected`. Only the selected stop is tabbable (roving tabindex); the arrow keys move it. Dragging moves the position with snapping and either neighbour-clamp or cross-and-re-sort per the root's `reorder`. Delete / Backspace removes it (a no-op at `minStops`). Give the stop an `aria-label` (defaults to `Stop N of M`). Exposes `{ stop, index, selected, percent }` as slot props.
| Prop | Type | Description |
|---|---|---|
stopId | string | The id of the stop this thumb represents. |
GradientEditorStops
StopsIterator part that keeps the DOM in sync with the root's sorted stops across add / remove / reorder. By default it renders a `GradientEditorStop` per entry (keyed by stop `id`). Provide a default slot to template each stop yourself — it receives `{ stop, index, selected }` and you wrap your markup in a `GradientEditorStop :stop-id="stop.id"`.
No props or events — renders its element and forwards attributes.
GradientEditorTrack
TrackThe gradient bar. Clicking an empty part of the track adds a stop at the click position (its color is interpolated from the neighbouring stops); pressing Enter / Space while the track is focused adds a stop at the center. It owns the shared `trackRef` used by `GradientEditorStop` for pointer ↔ position math and exposes the live `cssGradient` so consumers can paint the bar (e.g. as a CSS `background`). A press that lands on a stop is ignored here — the stop handles its own drag. Render the gradient preview yourself via the slot's `cssGradient`, e.g. `:style="{ background: cssGradient }"`.
No props or events — renders its element and forwards attributes.