Waveform
8 partsA 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).
Demo
Anatomy
Import the parts and compose them. Each part forwards attributes to its underlying element.
import {
WaveformRoot,
WaveformBars,
WaveformCursor,
WaveformEmpty,
WaveformPath,
WaveformRegion,
WaveformRegionHandle,
WaveformSelectionPreview,
} from '@robonen/primitives/canvas/waveform';
<WaveformRoot>
<WaveformBars />
<WaveformCursor />
<WaveformEmpty />
<WaveformPath />
<WaveformRegion />
<WaveformRegionHandle />
<WaveformSelectionPreview />
</WaveformRoot>API Reference
WaveformRoot
RootA 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).
| Prop | Type | Description |
|---|---|---|
peaks? | number[] | Float32Array | Per-sample amplitudes. Either normalized 0..1 magnitudes or signed
-1..1 PCM-style samples — select which via peaksRange. Length is
decoupled from duration: the root resamples by ratio to the bar count. |
peaksRange? | WaveformPeaksRange | The amplitude convention of peaks. '-1..1' (signed) is rectified via
absolute value; '0..1' is passed through. |
duration? | number | Total media duration, in seconds. |
currentTime? | number | Current playback position in seconds (v-model:current-time). |
regions? | WaveformRegionData[] | The set of regions (v-model:regions). |
offset? | number | Left edge of the visible window, in seconds (for timeline sync). |
zoom? | number | Horizontal zoom in pixels-per-second. 0 means "fit": the whole
[offset, duration] span fills the measured width. |
barWidth? | number | Bar thickness in pixels (bars mode). |
barGap? | number | Gap between bars in pixels (bars mode). |
mode? | WaveformMode | Body render strategy. |
createRegionOnDrag? | boolean | A press-drag on the body marquees out a new region instead of seeking. |
step? | number | Keyboard step for the cursor / region edges, in seconds. 0 means "1 pixel
of time" — resolved from the current projection. |
largeStep? | number | Large keyboard step (Shift+Arrow), in seconds. |
timeFormatter? | WaveformTimeFormatter | Default formatter for the cursor's aria-valuetext. |
loading? | boolean | Mark the waveform as loading async peaks (sets data-loading). |
disabled? | boolean | Disable all interaction. |
dir? | WaveformDirection | Writing direction. When omitted it is inherited from the nearest
ConfigProvider (falling back to 'ltr'); an explicit value wins. |
| Event | Payload |
|---|---|
update:currentTime | [value: number] |
update:regions | [value: WaveformRegionData[]] |
WaveformBars
BarsRenders the resampled bar geometry from `WaveformRoot` as a row of elements, one per bucket, each positioned and sized from the computed `buckets`. Purely presentational (`role="presentation"`, `aria-hidden`) — the accessible model lives on `WaveformCursor` and `WaveformRegion`. Bar heights are exposed as a `0..1` fraction (via CSS custom prop `--waveform-bar`) and as an explicit height percentage so consumers can style freely. Render this OR `WaveformPath`, per the root's `mode`.
No props or events — renders its element and forwards attributes.
WaveformCursor
CursorThe playback cursor, rendered as `role="slider"` over `[0, duration]` with `aria-valuenow` = the current time. It positions itself at `projection(currentTime)` and is fully keyboard-driven: Arrow Left/Right scrub by `step` seconds (Shift by `largeStep`), Home/End jump to `0`/`duration`, PageUp/PageDown seek by one visible window. Give it an `aria-label` (defaults to "Playback position"). `aria-valuetext` comes from `timeFormatter` (falling back to the root's, then `formatClock`).
| Prop | Type | Description |
|---|---|---|
timeFormatter? | WaveformTimeFormatter | Formatter for aria-valuetext. Overrides the root's formatter for this
cursor only. |
WaveformEmpty
EmptyPlaceholder shown when there is nothing to render — `duration === 0` or the peaks array is empty (the same condition that puts `data-empty` on the root), or while `loading` async peaks. Renders only in those states; the default slot receives `isEmpty` / `loading` so a consumer can show a spinner vs. a "no audio" message. Use this OR rely on the root's `data-empty` / `data-loading`.
| Prop | Type | Description |
|---|---|---|
whileLoading? | boolean | Also render while the root is loading. When false, only renders for the
empty (no peaks / zero duration) state. |
WaveformPath
PathAlternative SVG rendering of the waveform: a single smoothed `<path>` through the resampled peaks (mirrored top/bottom for a filled silhouette). Smoothing is Catmull-Rom (`buildSmoothPath`) at the supplied `tension`. Purely presentational (`aria-hidden`). Render this OR `WaveformBars`, per the root's `mode`. The `<svg>` stretches to the part's box; size it via CSS.
| Prop | Type | Description |
|---|---|---|
samples? | number | Number of points sampled across the body. Higher is smoother but costlier. |
tension? | number | Catmull-Rom tension forwarded to buildSmoothPath (0 = uniform). Higher
flattens the curve. |
filled? | boolean | Fill the area under the silhouette (mirrored) instead of stroking a line. |
WaveformRegion
RegionA single audio region (selection), rendered as `role="group"` whose `aria-label` is the formatted `start–end` range (plus the region `label` when set). It positions and sizes itself from `projection(start)..projection(end)` and hosts two `WaveformRegionHandle` children (one per edge) so trimming is keyboard-driven like a two-thumb range slider. The group itself can be moved by dragging its body; Enter/Space select it, Delete/Backspace remove it. Exposes `data-selected` / `data-active`.
| Prop | Type | Description |
|---|---|---|
regionId | string | The id of the region (in the root's regions) this part renders. |
draggable? | boolean | Whether the group body can be dragged to move the whole region. |
WaveformRegionHandle
RegionHandleOne trim handle of a `WaveformRegion`, rendered as `role="slider"` over `[0, duration]` with `aria-valuenow` = its edge's time. Pass `edge="start"` or `edge="end"`. Dragging trims that edge; Arrow Left/Right move it by `step` (Shift by `largeStep`), Home/End jump the edge to `0` / `duration`. Together the two handles behave like a two-thumb range slider over the region. Default `aria-label` is "Region start" / "Region end".
| Prop | Type | Description |
|---|---|---|
edge | WaveformRegionEdge | Which edge of the parent region this handle trims. |
WaveformSelectionPreview
SelectionPreviewThe transient marquee shown while a new region is being dragged out (`createRegionOnDrag`). Renders nothing until a create gesture is in flight, then positions/sizes itself from the in-progress selection. The pixel geometry is also exposed via the default slot so a consumer can render their own preview surface. `aria-hidden` (it has no committed value yet).
No props or events — renders its element and forwards attributes.