@robonen/primitives
A collection of unstyled, accessible UI primitives for Vue 3 — the headless building blocks for design systems and component libraries.
Most component libraries bundle behavior and styling together, so the moment your design diverges you end up fighting the framework. @robonen/primitives ships the hard part — state, focus management, keyboard interaction, ARIA wiring, portalling and positioning — and leaves the markup and styling entirely to you. Every primitive is composed from small, controllable parts (a Root, a Trigger, a Content, and so on) following the same conventions, so once you learn one you know them all.
Unstyled by design
No CSS shipped. Primitives render the DOM you ask for and expose state via data attributes, so you bring your own styles — Tailwind, vanilla CSS, anything.
Accessible out of the box
Focus scopes, roving tabindex, visually-hidden labels and correct ARIA roles are handled for you. The suite is tested against axe-core in a real browser.
Controlled or uncontrolled
Bind state with v-model when you need control, or set a defaultValue / defaultOpen and let the primitive manage itself.
Composable & polymorphic
Every part takes an as prop, or use as="template" to merge behavior onto your own element. Floating UI powers positioning for popovers, tooltips and menus.
Install
pnpm add @robonen/primitivesUsage
Primitives are assembled from named parts. Here is a complete dialog — open state is uncontrolled, focus is trapped, body scroll is locked, and the content is portalled out of the DOM flow:
<script setup lang="ts">
import {
DialogRoot,
DialogTrigger,
DialogPortal,
DialogOverlay,
DialogContent,
DialogTitle,
DialogDescription,
DialogClose,
} from '@robonen/primitives';
</script>
<template>
<DialogRoot>
<DialogTrigger class="btn">Open</DialogTrigger>
<DialogPortal>
<DialogOverlay class="overlay" />
<DialogContent class="dialog">
<DialogTitle>Delete project</DialogTitle>
<DialogDescription>This action cannot be undone.</DialogDescription>
<DialogClose class="btn">Cancel</DialogClose>
</DialogContent>
</DialogPortal>
</DialogRoot>
</template>Need full control over open state? Bind it directly — the same primitive works either way:
<DialogRoot v-model:open="isOpen">
<!-- ... -->
</DialogRoot>The Primitive component
At the core of every part is Primitive, a polymorphic functional component. Pass as to choose the element, or as="template" to forward behavior onto a child of your own.
import { Primitive, Slot } from '@robonen/primitives';
// <Primitive as="button" /> renders a <button>
// <Primitive as="template"> merges props onto the slotted childWhere to next
The full primitive index is listed below. A few good starting points:
- Dialog and Alert Dialog — modal layers with focus trapping.
- Popover, Tooltip and Hover Card — Floating UI positioned surfaces.
- Select, Combobox and Listbox — keyboard-driven option pickers.
- Switch, Checkbox and Slider — form controls that integrate with native inputs.
- Focus Scope and Presence — the shared foundations every part builds on.
forms · 12
A toggleable control with checked, unchecked, and `'indeterminate'` states, built on a native `<button role="checkbox">`. The interactive root: it owns the checked state (controlled via `v-model:checked` or uncontrolled via `defaultChecked`), handles toggling, exposes a hidden form input when `name` is set, and provides context to `CheckboxIndicator`. Use it whenever you need a styled checkbox that integrates with forms or supports a mixed/partial state. The checked value is generic: with the default `trueValue`/`falseValue` (`true`/`false`) it behaves as a boolean checkbox, but those props let the model carry arbitrary values (`'yes'`/`'no'`, objects, …) compared by deep equality. Nesting the root inside a `CheckboxGroupRoot` switches it to group mode: its checked state derives from membership in the group's array model and toggling adds/removes its `value`.
Inline-editable text field that toggles between a read-only preview and an editable input. Root owns the value (via `v-model`), edit state, and submit / cancel behavior, providing them to its parts. Use it for click-to-edit labels, titles, and table cells where a full form input would be heavy.
A caption associated with a form control. Renders a native `<label>` and, when `for` matches a control's id, lets clicks focus or toggle that control while announcing the text to assistive technology. Double-click text selection is suppressed so labels stay clickable. Use it to label inputs, checkboxes, switches, and other custom controls.
A numeric input with stepper controls, keyboard increment/decrement, and optional clamping. The interactive root: it owns the value (controlled via `v-model` / `update:modelValue` or uncontrolled via `defaultValue`), clamps to `min`/`max`, snaps to `step`, formats with the active locale, and provides context to `NumberFieldInput`, `NumberFieldIncrement`, and `NumberFieldDecrement`. Use it whenever you need a styled number entry with spinner buttons and arrow-key support.
A segmented input for short codes — OTP / one-time passwords, 2FA tokens, or PINs split across one box per character. The interactive root: it owns the value as a per-cell `string[]` (controlled via `v-model` / `update:modelValue` or uncontrolled via `defaultValue`), sizes the field to `length`, enforces the `type` ('text' | 'number') and `mask`, and provides context to each `PinInputInput`. Emits `complete` once every cell is filled. Use it for verification codes where each character gets its own cell with auto-advance, arrow-key navigation, and clipboard paste spreading across cells. Native form support: pass `name` (and optionally `required` / `id`) to render a visually-hidden form control holding the joined value, so the field submits with its owning `<form>` and participates in native `required` validation.
A set of mutually exclusive options where only one may be selected at a time, built on `role="radiogroup"` with full keyboard roving focus (arrow keys move and select, Space selects, Home/End and PageUp/PageDown jump to ends). The container and state owner: it tracks the selected value (controlled via `v-model` or uncontrolled via `defaultValue`), provides context to `RadioGroupItem`, and renders a hidden form input when `name` is set and the group lives inside a `<form>`. Values are not limited to strings — numbers, booleans, `null`, and plain objects are supported and compared structurally (override with `by`). Reach for it whenever a user must pick exactly one choice from a small, visible list.
An accessible slider for picking one or more numeric values from a range by dragging a thumb along a track or using the keyboard. The root owns the value state (controlled via `v-model` or uncontrolled via `defaultValue`), snaps to `step`, clamps to `min`/`max`, and handles pointer drags, focus, and arrow / Page / Home / End keys. Pass multiple values for a range (multi-thumb) slider and keep thumbs apart with `minStepsBetweenThumbs`; supports horizontal and vertical `orientation`, `dir`/`inverted` direction, and emits `valueCommit` when a drag or keypress settles. It provides context to `SliderTrack`, `SliderRange`, and `SliderThumb`, and renders hidden form inputs when `name` is set. Reach for it whenever a user should choose a value within known bounds (volume, price range, brightness).
A multi-step progress control that guides users through a sequence of steps — checkout flows, onboarding wizards, or any task split into ordered stages. Use it when you need to show where the user is, which steps are done, and (optionally) let them jump between steps. The root owns the active step (1-based), tracks the total via the Collection, arbitrates linear vs. free navigation, handles roving keyboard focus across triggers, and provides context to every `StepperItem`.
A control that toggles between an on and off state, mirroring a physical switch. Renders with `role="switch"`, exposes `data-state` and `data-disabled` for styling, and optionally mirrors its value into a hidden form input via `name`. The value is generic: it defaults to a boolean but can be any pair of `truthy`/`falsy` values (strings, numbers, objects compared by identity), and works uncontrolled (`defaultValue`) or controlled with `v-model`. Use it for instant settings toggles where the change applies immediately, as opposed to a checkbox that is typically submitted with a form. Pair it with `SwitchThumb` for the moving part: the thumb reads the switch context and mirrors `data-state`/`data-disabled`, enabling `data-[state=checked]` thumb animations.
A headless tags / token input: type a value, commit it on Enter (or paste, Tab, blur, or a custom delimiter), and manage the resulting list of tags with full keyboard navigation, duplicate/max guards, and accessible labelling. Use it for free-form multi-value entry such as email recipients, keywords, skills, or filter chips. Wraps the `Item`, `ItemText`, `ItemDelete`, `Input`, and `Clear` parts and provides their shared context.
A two-state button that can be pressed on or off, like a bold or italic control in a text editor toolbar. Renders a native `<button>` by default (handling Space/Enter and the `disabled` attribute for you), exposes `aria-pressed`, `data-state` (`on`/`off`), and `data-disabled` for styling, and works uncontrolled (`defaultPressed`) or controlled via `v-model:pressed`. When rendered as a non-button element it synthesizes keyboard activation and the appropriate `tabindex`/`aria-disabled`. Provide `name` to mirror the pressed state into a hidden checkbox so it participates in native form submission (suppressed automatically when nested in a `ToggleGroup`, which owns the submitted value). Use it for a single standalone toggle; for a set of mutually related toggles use `ToggleGroup` instead.
A set of two-state toggle buttons that behave as one control, with full keyboard roving focus (arrow keys move, Home/End jump to ends, PageUp/PageDown jump to first/last). Set `type` to `'single'` for mutually exclusive options (like a segmented control) or `'multiple'` to let several be pressed at once (like a text-formatting bar). When `type` is omitted it is inferred from the value shape: an array value implies `'multiple'`, otherwise `'single'`. This is the container and state owner: it tracks the pressed value(s) (controlled via `v-model` or uncontrolled via `defaultValue`) and provides context to each `ToggleGroupItem`. With a `name`, the selected value(s) are also bridged into native form submission. Reach for it to group related toggles such as text alignment, view modes, or formatting options.
selection · 3
An autocomplete / typeahead input that filters a list of options as the user types. Combine a text input with a popup listbox, supporting single or multiple selection, custom filtering, and full keyboard navigation. Reach for it when users must pick from a large or searchable set of options; for a small fixed list a plain Select is simpler. Wraps everything in a Popper and provides shared state to every other Combobox part.
A list of selectable options that supports single or multiple selection, full keyboard navigation (arrows, Home/End, PageUp/PageDown, type-ahead), Shift-range selection, and optional hover highlighting. Use it when you need an always-visible selection list — picking from a set of values, building a custom multi-select, or as the options surface inside a larger widget. The root owns selection state (controlled via `v-model` or uncontrolled via `defaultValue`), the highlighted item, orientation/direction, optional native-form integration (`name`/`required`), and provides context to every descendant part.
A custom, fully stylable replacement for the native `<select>` element: a trigger button that opens a floating listbox of options, with full keyboard support (arrow keys, Home/End, type-ahead search), focus trapping, and an optional hidden native `<select>` for native form submission. Use it when you need a single- or multi-choice dropdown whose menu and options must be styled beyond what a native control allows. The root owns the selected value and open state and provides context to every part; bind `v-model` for the value and `v-model:open` (or listen to `update:modelValue` / `update:open`) to control or observe it. Values may be strings, numbers, booleans, or objects (compared via `by`). Compose it from a `SelectTrigger` (with `SelectValue`/`SelectIcon`) plus a portalled `SelectContent` of `SelectItem`s.
color · 4
A 1D slider for picking the alpha (opacity, `0–1`) of a colour. It works standalone — owning its own `HSVA` via `v-model` / `defaultValue` — or, when nested inside a `ColorFieldRoot`, reads and writes that shared colour so the whole picker cluster stays in sync. Mirrors the standard slider anatomy: the root owns the value, maps pointer drags along the track, handles arrow / Page / Home / End keys, and provides context to `AlphaSliderThumb`. The background should be a checkerboard overlaid with an opaque→transparent colour gradient; style it via the exposed slot/`data-*` hooks. Reach for it as the opacity rail of a colour picker.
The 2D saturation/value square of a colour picker. The x-axis maps saturation (`0` at the left → `1` at the right) and the y-axis maps brightness/value (`1` at the top → `0` at the bottom). It works standalone — owning its own `HSVA` via `v-model` / `defaultValue` — or, nested inside a `ColorFieldRoot`, reads and writes that shared colour so the whole picker cluster stays in sync. A pointer press anywhere in the area sets saturation and brightness at once; the `--color-area-hue` CSS variable is exposed so the consumer can paint the full-saturation / full-value hue background. Provides context to `ColorAreaThumb`. Reach for it as the main square of a colour picker.
The composite root of the colour-picker cluster. It owns the canonical {@link HSVA} colour (controlled via `v-model`, uncontrolled via `defaultValue`) and provides a shared context that `ColorArea`, `HueSlider`, and `AlphaSlider` read and write into, keeping every control in sync without round-tripping through RGB. The model accepts either an `HSVA` object or any CSS colour string (`#rrggbb`, `rgb()/rgba()`, `hsl()/hsla()`) via `parseColor` and emits in the configured `format`. Compose it with `ColorFieldSwatch`, `ColorFieldInput`, `ColorFieldLabel`, and `ColorFieldHiddenInput`. Reach for it whenever you need a full, accessible colour picker tied to a form value.
A 1D slider for picking the hue (`0–360°`) of a colour. It works standalone — owning its own `HSVA` via `v-model` / `defaultValue` — or, when nested inside a `ColorFieldRoot`, reads and writes that shared colour so the whole picker cluster stays in sync. Mirrors the standard slider anatomy: the root owns the value, maps pointer drags along the track, handles arrow / Page / Home / End keys, and provides context to `HueSliderTrack` and `HueSliderThumb`. The gradient background should run through the full hue wheel; style it via the exposed slot/`data-*` hooks. Reach for it as the hue rail of a colour picker.
overlays · 7
A modal dialog that interrupts the user with important content and expects a deliberate response. Built on top of Dialog, but always modal and rendered with `role="alertdialog"` — focus moves to the Cancel button on open and outside clicks are ignored, so the user must explicitly confirm or cancel. Use it for destructive or irreversible actions (deleting data, discarding changes); for non-blocking content prefer Dialog instead. Manages open state and provides context to all parts. Bind `v-model:open` to control it.
A window overlaid on the page that interrupts the rest of the app while it is open — used for tasks like forms, confirmations, or detail views that should sit above the current context. Composed from a Trigger, a Portal, an Overlay, and Content (with Title, Description, and Close). Root manages the open state and provides context to every part. Bind `v-model:open` to control it, or rely on the Trigger/Close for uncontrolled use. Modal by default (traps focus, locks scroll, marks the rest of the document inert); set `modal="false"` for a non-blocking dialog. For destructive confirmations that demand an explicit choice, prefer AlertDialog.
A rich, floating card that previews related content when the pointer hovers (or keyboard focus lands) on a trigger, after a short open delay. Built on Popper for collision-aware positioning, with a grace area so the pointer can travel from the trigger to the card without it closing. Use it for sighted-user preview affordances — a user profile on an
A floating panel anchored to a trigger, used for rich, interactive content such as forms, settings, or detail cards that can hold focusable elements. Composed from a Trigger, an optional Anchor, a Portal, and Content (with an optional Arrow and Close). Positioning is handled by the underlying Popper. Root manages the open state and provides context to every part. Bind `v-model:open` to control it, or rely on the Trigger/Close for uncontrolled use. Non-modal by default; set `modal` to trap focus, lock scroll, and block outside pointer events. Reach for a Popover when you need interactive overlay content; use Tooltip for hover-only labels and Dialog for blocking, page-level tasks.
The context provider for a popper. It coordinates positioning between `PopperAnchor` (the reference element), `PopperContent` (the floating element placed against it), and `PopperArrow`, sharing the registered anchor via context. It renders only its slot and adds no DOM of its own, so wrap the anchor and content in it to build tooltips, popovers, dropdown menus, and other floating UI.
A small floating label that appears on hover or keyboard focus to describe an otherwise non-obvious control (such as an icon-only button). Composed from a Trigger, a Portal, and Content (with an optional Arrow); positioning is handled by the underlying Popper. Tooltips are pointer/focus driven and non-interactive by design — reach for Popover when the overlay needs focusable content. Root owns the per-tooltip open state and provides context to every part. Each Root must live inside a `TooltipProvider`, which supplies shared delay/skip timing for a group of tooltips. Bind `v-model:open` to control it, or rely on the Trigger for uncontrolled use. Props here override the matching Provider defaults for this one tooltip.
menus · 7
Root of a command palette / fuzzy-finder menu (cmdk-style): owns the search term, the registry of items and groups, scoring/filtering, and keyboard-driven highlight + selection. Compose it with `CommandInput`, `CommandList`, `CommandGroup`, `CommandItem`, `CommandEmpty`, `CommandLoading`, and `CommandSeparator`. Reach for it whenever you need a searchable, keyboard-first list of actions or options — a Spotlight-style launcher, an autocomplete menu, or a quick-switcher.
A menu that opens at the pointer on right-click (or a long-press on touch), replacing the platform's native context menu with your own styled actions. Built on top of Menu, so it inherits keyboard navigation, typeahead, nested submenus, and checkbox/radio items. Use it for contextual actions tied to a region or element — cut/copy/paste, row actions in a table, canvas tools — when there is no persistent button to click. The root owns open state and provides context to every part; listen to `update:open` to react when the menu opens or closes.
A button-triggered menu of actions, opened on click and built on top of Menu, so it inherits keyboard navigation, typeahead, nested submenus, and checkbox/radio items. Unlike a context menu, it is anchored to a persistent trigger button rather than the pointer. Use it for action menus on a toolbar, an avatar, or a "more" button — settings, row actions, account menus, and the like. The root owns open state and provides context to every part; bind `v-model:open` (or listen to `update:open`) to control or observe whether the menu is open.
The unstyled, low-level menu engine that powers DropdownMenu, ContextMenu, and Menubar. It is built on Popper and wires up roving-focus keyboard navigation, typeahead, nested submenus, checkbox/radio items, and modal vs. non-modal dismissal — but it is deliberately trigger-agnostic, so consumers supply their own anchor and open logic. Use this directly only when composing a new menu-like primitive; for ordinary app menus reach for DropdownMenu or ContextMenu instead. MenuRoot owns open state and provides context to every part; bind `v-model:open` (or listen to `update:open`) to control or observe whether the menu is open.
A horizontal bar of menus, like the File / Edit / View row in a desktop app. Each MenubarMenu owns a trigger and its dropdown; the root coordinates them so only one is open at a time, arrow keys move between triggers, and typeahead jumps to a trigger by name. Built on top of Menu, so every menu inherits keyboard navigation, nested submenus, and checkbox/radio items. Use it for application-style menu bars in editors, dashboards, and tools. The root holds which menu is open; bind `v-model` (or listen to `update:modelValue`) to control or observe the active menu's value.
A collection of navigation links and disclosure menus, typically used for the primary site header. `NavigationMenuRoot` owns the open state and hover/click timing for the whole menu, rendering as a `<nav>` landmark and providing context to every list, item, trigger, content, viewport, and indicator beneath it. Reach for it over a generic dropdown when you need keyboard-accessible, animatable mega-menu panels that share a single active state.
A container that groups a set of controls — buttons, toggles, links, separators — into a single keyboard-navigable strip (`role="toolbar"`). Like an editor's formatting bar, the whole toolbar is one tab stop: Tab moves into it, then arrow keys roam between items (Home/End and PageUp/PageDown jump to the ends), with optional wrap-around via `loop`. It owns the roving-focus state, exposes `data-orientation` for styling, and provides context to every `ToolbarButton`, `ToolbarLink`, `ToolbarToggleGroup` and `ToolbarSeparator`. Reach for it to assemble action bars, formatting toolbars, or any cluster of related controls.
disclosure · 3
An interactive component that expands and collapses a panel of content. `CollapsibleRoot` owns the open/closed state (controlled via `v-model:open` or uncontrolled via `defaultOpen`), provides it to the `Trigger` and `Content` parts, and reflects it as `data-state`. Use it for show/hide disclosures such as "read more" sections, FAQ entries, or settings panels.
A set of layered sections of content — known as tab panels — where only one panel is shown at a time, each surfaced by its own trigger. Use it to split related content into switchable views without leaving the page: settings panes, dashboards, or product detail sections. The root owns the selected value (controlled via `v-model` or uncontrolled via `defaultValue`), orientation, keyboard roving focus across triggers, and provides context to every `TabsList`, `TabsTrigger`, and `TabsContent`.
display · 8
Displays content within a fixed, responsive width-to-height ratio. The element grows to fill its container's width and derives its height from the `ratio`, so the box keeps its proportions at any size. Use it to reserve layout space for images, video, maps, or embeds and avoid content shift.
An image element representing a user, with a graceful text/icon fallback for when the image is loading or fails to load. Use it for profile pictures in avatars, comment threads, member lists, or anywhere a user identity is shown and you need a reliable placeholder. The root tracks the image's loading status and provides it via context so `AvatarImage` and `AvatarFallback` can coordinate which one is rendered. It exposes the current status on the `data-status` attribute for styling.
A fully accessible, headless date calendar for picking a single day. The root owns the selected value and the displayed month ("placeholder"), builds the localized month grid(s), and wires up roving keyboard navigation, min/max bounds, and disabled/unavailable predicates. Use it to build an inline date picker or as the body of a popover/`DatePicker`. Compose it with `CalendarHeader` (`CalendarPrev` / `CalendarHeading` / `CalendarNext`) and one `CalendarGrid` per month. Supports `v-model` for the selected date and `v-model:placeholder` for the visible month.
A single-date picker that pairs a popover-anchored calendar with an optional trigger, field, and hidden form input. Owns the selected date, placeholder month, and open state, and provides both date-picker and calendar context to its parts. Use it when you need a compact, accessible "pick one date" control (e.g. a form field) rather than an always-visible `Calendar`.
A bar that shows the completion progress of a task, typically a horizontal fill that grows from empty to full. Use it for file uploads, multi-step form progress, loading indicators, or any operation whose progress you can measure (or, with a `null` value, signal as indeterminate). The root renders the accessible `progressbar` (wiring up `aria-valuemin`, `aria-valuemax`, `aria-valuenow`, `aria-valuetext`, and an `aria-label` accessible name) and derives the current `state` — `indeterminate`, `loading`, or `complete` — which it provides via context and exposes on the `data-state` attribute. Pair it with `ProgressIndicator` for the visual fill. Both `modelValue` and `max` are two-way (`v-model` / `v-model:max`): bad inputs (`NaN`, negatives, out-of-range, `max <= 0`) are validated, clamped, and reported in development, so the rendered ARIA is always valid.
The root of a QR code. Encodes `value` into a matrix (via `@robonen/encoding`) and renders an `<svg>` whose viewBox is laid out in module units, so every child part draws in the same resolution-independent coordinate space and the whole code scales with the SVG's CSS width/height. It is fully headless: compose `QrCodeBackground`, `QrCodeCells`, `QrCodeMarkers`/`QrCodeMarker` and `QrCodeLogo` inside it and style them with CSS (`fill`, gradients, `<defs>`) — patterns, marker shapes and logos are all controlled by props or slots on those parts.
Provides a styleable, cross-browser scroll container that swaps native scrollbars for custom ones while preserving native scrolling, keyboard, and accessibility behaviour. The root holds shared state and renders nothing visible on its own — compose it with a `ScrollAreaViewport` (the scrollable region), one or two `ScrollAreaScrollbar`s (each containing a `ScrollAreaThumb`), and an optional `ScrollAreaCorner`. Reach for it when the default OS scrollbars clash with your design or differ across platforms.
A thin visual divider that separates and gives meaning to groups of content, such as items in a menu, sections of a toolbar, or rows in a list. Renders horizontally or vertically and, unless marked `decorative`, exposes `role="separator"` with the matching `aria-orientation` to assistive technology. Use it to break up related content into distinct regions. For slot-merging composition (rendering onto a consumer-provided element instead of an extra wrapper) pass `as="template"` — the underlying `Primitive` merges its props/ref onto the single child of the default slot.
feedback · 1
canvas & editors · 15
An accessible circular angle / rotation picker. The root owns the angle value in DEGREES (controlled via `v-model:value` or uncontrolled via `defaultValue`), converts pointer presses anywhere on the dial into an angle, snaps to `snap` / `step`, and handles the `0` / `360` seam either by wrapping continuously (`wrap`) or bounding to an arc (`clamp`). It provides context to `AngleDialThumb`, which renders the `role="slider"` handle on the ring and owns keyboard interaction. The angle convention is fixed: `0°` points UP (12 o'clock) and increases CLOCKWISE (right = 90°, down = 180°, left = 270°). Reach for it for rotation, heading, or hue (a hue ring is `min: 0, max: 360, wrap: 'wrap'`).
Root of a headless pan/zoom **canvas stage** — a thin photo-editing shell over the `zoom-pan` viewport that adds the three classic fit modes (**fit** / **1:1** / **fill**) on top of pan + zoom, plus automatic content-size measurement so those modes work without the consumer hand-feeding dimensions. It owns the master `Viewport` (two-way via `v-model:viewport`, or uncontrolled via `defaultViewport`), renders an internal `ViewportRoot` wired with the same model + zoom constraints + the resolved content extent, and builds the {@link CanvasStageContext} that wraps the zoom-pan {@link ViewportApi} and adds `fitView()` / `zoomToActual()` / `fitFill()` + the reactive content size. The combined api is `defineExpose`d so consumers can drive it (and wire their own zoom buttons) via a template ref. Carries `role="application"` (downgraded to `'group'` when keyboard a11y is disabled), `aria-roledescription="zoomable canvas"`, and `tabindex 0`; pass an `aria-label` via `$attrs`. Mount your `<img>` / `<video>` / `<canvas>` in the default slot — it renders inside the single transformed layer.
A before/after split-reveal slider: two stacked layers (a base `CompareSliderBefore` and a clipped `CompareSliderAfter`) with a draggable divider that reveals exactly `position`% of the after-layer. The root owns the reveal position (controlled via `v-model:position` or uncontrolled via `defaultPosition`), clamps it to 0–100, and starts a pointer drag on press — mapping the pointer's position over the root's box onto the reveal percentage. When `hover` is set the divider follows the pointer on hover (no press needed). It provides context to `CompareSliderBefore`, `CompareSliderAfter`, `CompareSliderHandle`, and `CompareSliderDivider`, and supports horizontal/vertical `orientation` plus `dir`/`inverted` direction. Reach for it to compare two images, designs, or any two overlaid views.
Headless crop selector rendered **over** a media element (`<img>`, `<video>`, `<canvas>`). It owns a single crop rectangle — controlled via `v-model` or uncontrolled via `defaultValue` — and drives moving, eight-handle resizing, aspect-ratio locking, a rule-of-thirds grid, and a draw-from-empty create gesture. The rect lives in NORMALIZED `0..1` fractions of the media by default (resolution-independent) or in media pixels via `units: 'pixels'`. Supply the media size with `mediaWidth`/`mediaHeight` for standalone use, or mount inside a `CanvasStageRoot` and the Root reads the stage's content size automatically (props still win when given). It provides {@link CropContext} to `CropArea`, `CropHandle`, `CropGrid`, and `CropOverlay`, and emits `cropCommit` when a gesture or keypress settles. Reach for it to let a user pick a sub-rect of an image or video (avatar crop, thumbnail framing, redaction region).
A headless control-point curve editor: a draggable set of anchors defining a single-valued `y = f(x)` curve. It backs both **animation easing curves** (an ease over normalized time) and **photo tone curves** (per-RGB-channel output remapping), and is the shared engine reused by Levels (gamma) and the future KeyframeTrack. The root owns the anchor array (controlled via `v-model`, uncontrolled via `defaultValue`), builds value↔pixel projections for both axes (`useScale`, y-axis value-up), and exposes the live evaluator: `sample(x) → y` and `toLUT(size)` for applying the curve to pixels. With `monotonicX` (the default) anchors are neighbour-clamped so they can never cross in x — easing and tone curves both require a function of x. `fixedEndpoints` locks the first and last anchor in x. The `interpolation` mode selects monotone (default), linear, catmull-rom, or per-anchor bezier handles. Provides context to `CurveEditorGrid`, `CurveEditorCurve`, `CurveEditorPoint`, and `CurveEditorHandle`. The `channel` prop only tags which curve is being edited (for styling / the `#channel` slot); consumers render their own RGB tabs.
Root of the headless flow canvas. Owns node/edge/viewport state (two-way via `v-model:nodes` / `v-model:edges` / `v-model:viewport`, or uncontrolled via `defaultNodes` / `defaultEdges` / `defaultViewport`), reconciles the public arrays into internal `shallowRef` Maps for O(1) reads, and provides `FlowContext` to every part. It renders the standard `FlowPane → FlowViewport → (edges, nodes)` subtree and exposes the default slot for absolutely- positioned chrome (Background / Controls / MiniMap / Panel). Customise node and edge rendering with `nodeTypes` / `edgeTypes` component maps or the `#node-<type>` / `#edge-<type>` scoped slots. Emits granular `@nodes-change` / `@edges-change` alongside `v-model`, so consumers may own their data.
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).
A headless, accessible per-channel image histogram. The root owns the bin `data` (single-channel `number[]` or a per-channel record), normalises each channel against its own peak under the chosen `scaleType` (`'linear'` or `'log'`), and provides the resulting `[0, 1]` bar heights to `HistogramBars`. It is a dense visual: the rendered bars are `aria-hidden`, while the root carries `role="img"` with an `aria-label` summary (or `role="group"`). The all-zero / empty guard is built in — a flat or empty histogram projects to zero height (no divide-by-zero, no `NaN`) and the summary label reports "no data". Pair it with `LevelsRoot` for a Photoshop-style levels editor, or use it standalone to visualise tonal distribution.
Root of the headless keyframe track: animation keyframes laid out on a time axis, each segment carrying an editable cubic-bezier easing. It owns the keyframe array (two-way via `v-model` or uncontrolled via `defaultValue`), builds the time↔pixel projection (its own `useScale` when standalone, or the injected Timeline's scale when nested as a lane), and exposes the live sampler `sampleAt(time)` plus the easing editor binding. Transient drag positions are written to an in-flight overlay and committed on pointerup (`commit`); an external `v-model` write during a gesture is ignored (the `isMutating` early-return) so it never clobbers the live drag — mirroring the Timeline reconcile. Provides `KeyframeTrackContext` to every part: the projection, the shared frame-grid snap engine, the keyframe actions, and the roving-focus registry. When nested in a Timeline it derives `duration` / `fps` from that context and renders as a `listitem`; standalone it measures its own lane and renders as a `group`.
A headless, accessible Photoshop-style levels control. The root owns a `LevelsValue` — input `black`/`white` clipping points (`0..255`), a `gamma` midtone factor (`0.1..9.99`), and an `outputBlack`/`outputWhite` range (`0..255`) — controlled via `v-model` or uncontrolled via `defaultValue`. It is a constrained multi-thumb slider: `black` is kept strictly below `white` (by `minStepsBetweenHandles * step`) and the output handles keep their order, with a value pushed past its neighbour pinning rather than swapping. The root handles pointer drags and keyboard for the thumbs, exposes the `0..255` output LUT via `getOutputCurve`, and can derive auto black/white from a histogram via `autoLevels`. Provides context to `LevelsTrack`, `LevelsThumb`, and `LevelsHandleValue`. Pair it with `HistogramRoot` for a full levels editor.
Root of the headless multi-track timeline (video/audio editor). It owns the track / clip / marker data plus the playhead (`currentTime`), horizontal scroll (`offset`), zoom (`pxPerSecond`), and selection — all two-way via `v-model` or uncontrolled via `default*`. The public arrays are reconciled into internal `shallowRef` Maps for O(1) reads and identity-stable per-item computeds. The COORDINATE MODEL is load-bearing: `pxPerSecond` is applied as real horizontal LAYOUT pixels (clip widths are genuine px, never a CSS `scale(zoom)`), so a `useScale` projects the visible window `[offset, offset + width/pxPerSecond]` → `[0, width]`. The vertical axis is fixed-height track lanes and is NOT zoomed. Transient drag/trim/scrub positions are written to an in-flight overlay and committed to the model on pointerup (`commitMutation`); an external `v-model` write during a gesture is ignored (the `isMutating` early-return) so it never clobbers the live drag. Granular `@clips-change` / `@tracks-change` are emitted alongside `v-model` so consumers may own their data via `applyClipChanges` / `applyTrackChanges`. Provides `TimelineContext` to every part: the scale, the shared snap engine (clip edges + playhead + markers + grid), the clip/track/playhead actions, and the roving-focus registry.
A headless, zoomable time axis: a horizontal ruler of ticks and labels over a span of `duration` seconds. It renders the accessible `group` (region when labelled), measures its own width, and builds a `useScale` whose domain is the visible time window `[offset, offset + width / zoom]` and range `[0, width]`. The visible window is driven by two models — `offset` (the left-edge time in seconds, `v-model:offset`) and `zoom` (pixels-per-second, `v-model:zoom`) — which stream continuously while panning / zooming; the root additionally emits SETTLE events (`panCommit`, `zoomCommit`, `rangeChange`) when a gesture ends. Tick generation is selected by `mode`: `'seconds'` uses the human time ladder, `'timecode'` renders `HH:MM:SS:FF` SMPTE labels at `fps` (drop-frame optional), and `'frames'` renders integer frame numbers. When focusable the root handles a keyboard layer (Arrow keys pan, Shift+Arrow pans by a major interval, `+` / `-` zoom about the canvas centre) and optional wheel / drag panning. It is usable standalone or embedded in a `Timeline` via `TimeRulerContext`, which exposes the tick collections, the `scale` / `invert` projectors, the `offset` / `zoom` models, and the `formatTime` helper. The default slot surfaces `{ ticks, majorTicks, minorTicks, scale, formatTime }` so consumers can render their own tick layer; the `TimeRuler*` parts are opt-in.
A headless move / scale / rotate bounding box. The root owns the transform `{ x, y, width, height, rotation }` (controlled via `v-model` or uncontrolled via `defaultValue`), sizes and rotates itself to it, and provides the gesture machinery to its handle parts: `TransformBoxHandle` (8 scale handles), `TransformBoxRotateHandle`, and the optional `TransformBoxStatus` live region. All math (rotated-box resize, aspect lock, flip, rotation) lives in pure helpers in `./utils` so Crop can share `resizeEdge`/`constrainRect`/etc. without importing any component. The body itself is draggable (move) and keyboard-focusable (arrow-move); handles delegate their gesture math here. Reach for it whenever a user repositions, resizes, or rotates a free object on a canvas (image, shape, text frame, crop region).
A headless audio waveform: it renders amplitude peaks as bars (or a smoothed path), overlays a draggable playback cursor, and hosts zero or more draggable regions (selections). The root owns the peaks, the `currentTime` model, and the `regions` model; it measures its own width, builds a time↔pixel projection over the visible window (`offset` + `zoom` for timeline sync), and resamples `peaks` to the available bar count by ratio (never assuming one peak per pixel). Pointer-down on the body scrubs the cursor; with `createRegionOnDrag` a press-drag marquees out a new region. It provides context to `WaveformBars`/`WaveformPath`, `WaveformCursor`, `WaveformRegion`, `WaveformSelectionPreview`, and `WaveformEmpty`. Reach for it to visualize and navigate audio (players, trimmers, transcript editors).
utilities · 6
A low-level building block that detects when the user interacts away from its content — pressing Escape, clicking/pointing outside, or moving focus out — and emits a `dismiss` event so the consumer can close the layer. Layers are tracked in a global stack so only the topmost one responds, letting dialogs, popovers, menus, and tooltips nest correctly. Use it to wrap any transient overlay whose lifecycle you want driven by outside-interaction; it renders no UI of its own.
A low-level building block that manages keyboard focus for its contents. On mount it can move focus inside (autofocus), on unmount it restores focus to the previously focused element, and while active it can loop Tab/Shift+Tab at the edges and/or trap focus so it cannot leave the container. Scopes are tracked in a global stack so nested scopes (e.g. a dialog opening over a popover) hand focus management back and forth correctly. Use it to wrap any overlay or modal surface that needs accessible focus containment; it renders no UI of its own. Emits `mountAutoFocus`/`unmountAutoFocus` so the consumer can override the default focus target.
Controls the mount/unmount lifecycle of a single child while respecting CSS enter/leave animations, keeping it in the DOM until any exit animation has finished. Use it as the foundation for show/hide parts (dialog content, tooltips, collapsible panels) so they animate out before being removed, rather than disappearing instantly when their `present` state flips to false. `Presence` is a headless building block: it renders its child via `Slot` (merging attributes onto it), wires up animation tracking, and exposes the resolved presence to the default slot so children can reflect it (e.g. as a `data-state`). The same logic is available standalone through `usePresence`.
Renders its slot content into a different part of the DOM (a portal), wrapping Vue's built-in `<Teleport>` with SSR-safe defaults and a configurable target. Use it as the building block for overlay primitives (dialogs, popovers, toasts) that must escape parent overflow/stacking contexts; the target defaults to the `teleportTarget` from `ConfigProvider` (`body` unless overridden).
Visually hides its content while keeping it available to assistive technology. The element is removed from the visual layout but stays in the accessibility tree (and remains focusable) so screen readers can still announce it. Use it for accessible labels, status text, or skip links that should be heard but not seen — for example a hidden heading, an icon-only button's name, or extra context for a control.