GradientEditor

6 parts

The 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

Loading demo…

Anatomy

Import the parts and compose them. Each part forwards attributes to its underlying element.

vue
import {
  GradientEditorRoot,
  GradientEditorAngle,
  GradientEditorColorEditor,
  GradientEditorStop,
  GradientEditorStops,
  GradientEditorTrack,
} from '@robonen/primitives/canvas/gradient-editor';

<GradientEditorRoot>
  <GradientEditorAngle />
  <GradientEditorColorEditor />
  <GradientEditorStop />
  <GradientEditorStops />
  <GradientEditorTrack />
</GradientEditorRoot>

API Reference

GradientEditorRoot

Root

The 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).

PropTypeDescription
defaultValue?GradientStop[]Uncontrolled initial stops.
type?GradientTypeGradient type.
defaultAngle?numberUncontrolled initial linear angle in degrees.
defaultSelectedId?string | nullUncontrolled initial selected stop id.
minStops?numberMinimum number of stops; removal is blocked at this floor.
reorder?booleanWhether 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?numberKeyboard step (fraction) for Arrow nudges.
largeStep?numberLarge keyboard step (Shift+Arrow / Page keys).
snapStep?numberOptional grid snap increment (fraction) applied to drags and keyboard moves. undefined disables grid snapping.
disabled?booleanDisable all interaction.
dir?GradientEditorDirectionWriting direction. When omitted it is inherited from the nearest ConfigProvider (falling back to 'ltr'); an explicit value wins.
valueText?GradientEditorValueTextOptional per-stop aria-valuetext formatter.
modelValue?GradientStop[] | nullTwo-way bound value (v-model).
angle?number | nullTwo-way bound value (v-model:angle).
selectedId?string | nullTwo-way bound value (v-model:selectedId).
Emits
EventPayload
update:modelValue[value: GradientStop[] | null]
update:angle[value: number | null]
update:selectedId[value: string | null]

GradientEditorAngle

Angle

Optional 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 }`).

PropTypeDescription
min?numberMin angle in degrees.
max?numberMax angle in degrees.
step?numberStep granularity in degrees.
largeStep?numberLarge step (Page keys / Shift+Arrow) in degrees.

GradientEditorColorEditor

ColorEditor

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

PropTypeDescription
format?ColorFormatSerialization format written back to the stop's color.

GradientEditorStop

Stop

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

PropTypeDescription
stopIdstringThe id of the stop this thumb represents.

GradientEditorStops

Stops

Iterator 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

Track

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