Lab

Custom elements

Every primitive as framework-agnostic Web Components, with the parts, state attributes and events of the React primitives.

@audio-ui/elements provides every primitive as a set of unstyled, accessible custom elements. Each one is built on @audio-ui/core and keeps API parity with @audio-ui/react: the same parts, keyboard interactions, ARIA semantics and data-* state attributes. The elements work in plain HTML and in any framework that renders HTML, such as Vue, Svelte, Angular or Lit. Every primitive in the API reference has a live preview.

Installation

pnpm add @audio-ui/elements

Each primitive is a separate entry point, @audio-ui/elements/<primitive>, which registers its elements when imported. The package root registers every primitive. Import an entry once, before the elements are used:

import "@audio-ui/elements/knob";

Registration is a no-op on the server: server-rendered markup upgrades once the module loads in the browser.

Usage

<audio-ui-knob min="20" max="20000" default-value="1000" scale="log">
  <audio-ui-knob-label>Cutoff</audio-ui-knob-label>
  <audio-ui-knob-control>
    <audio-ui-knob-track></audio-ui-knob-track>
    <audio-ui-knob-indicator></audio-ui-knob-indicator>
    <audio-ui-knob-thumb></audio-ui-knob-thumb>
  </audio-ui-knob-control>
  <audio-ui-knob-value></audio-ui-knob-value>
</audio-ui-knob>

<script type="module">
  import "@audio-ui/elements/knob";

  const knob = document.querySelector("audio-ui-knob");
  knob.addEventListener("input", (event) => setCutoff(event.detail.value));
</script>

Value and events

Every control follows the same contract:

MemberKindDescription
inputEventFires on every change. event.detail holds the new value and the reason for the change, such as "drag", "keyboard", "wheel", "reset" or "input-change". Calling preventDefault() rejects the change.
changeEventFires once when an interaction that changed the value ends. Use it to commit: start a note, record an undo step, save.
valuePropertyReads the current value, and sets it. Framework bindings (:value in Vue, value={} in Svelte) write this property. During a drag, the control follows the pointer even when the bound value lags behind.
nameAttributeSubmits the value with the enclosing form, through the control's native range input.

A part that holds a value of its own fires events prefixed with its name, for example clipinput and clipchange on a timeline clip. These never collide with the control's own input and change, and they bubble, so a single listener on the root can handle every part.

Styling

The elements apply only the styles their behavior depends on: the display of each box, the positioning of placed parts, touch-action on interactive surfaces. These rules are wrapped in :where(), so they carry no specificity and any rule of yours takes precedence. No color, size or spacing is set. Style the parts by tag and by the same state attributes as the React parts:

audio-ui-knob-control { width: 4rem; height: 4rem; cursor: grab; }
audio-ui-knob[data-dragging] audio-ui-knob-control { cursor: grabbing; }
audio-ui-knob-track, audio-ui-knob-indicator { position: absolute; inset: 0; }
audio-ui-knob-track { color: var(--muted); }
audio-ui-knob-indicator { color: var(--primary); }
audio-ui-knob-thumb:has(input:focus-visible) { outline: 2px solid var(--ring); }

Parts drawn as SVG use currentColor. Classes work as well: the knob and fader previews apply the registry's Tailwind classes.

API reference

Knob

<audio-ui-knob> is the root of a rotary control. It owns the value and renders its parts from it.

Props · 13

PartDescription
<audio-ui-knob-control>The interactive surface: pointer drag, wheel, double tap or Alt + click to reset
<audio-ui-knob-track>The full sweep, drawn as an SVG arc. stroke-width sets its width in the 48×48 viewBox (3 by default)
<audio-ui-knob-indicator>The arc from anchor to the value
<audio-ui-knob-thumb>Rotates with the value and contains the native range input, to which its aria-label, aria-labelledby and aria-describedby are moved
<audio-ui-knob-value>The formatted value
<audio-ui-knob-label>Labels the input when the thumb has no accessible name

Fader

<audio-ui-fader> is the root of a linear control, with audio defaults: vertical, from -60 to +6. It has role="group", labelled by its label part.

Props · 12

PartDescription
<audio-ui-fader-control>The interactive area. A press on the track jumps to the pointer; a press on the thumb drags from where it was grabbed; the wheel steps
<audio-ui-fader-track>The full range of travel
<audio-ui-fader-indicator>The filled range, from min or anchor to the value
<audio-ui-fader-thumb>The draggable thumb, positioned at the value, containing the native range input
<audio-ui-fader-value>The formatted value
<audio-ui-fader-label>Labels the group, and the input when the thumb has no accessible name

Pad

<audio-ui-pad> is a grid of velocity-sensitive pads with role="group". A pad sounds while held, by pointer or with Enter and Space, and each pointer holds its own pad. Velocity comes from pen or touch pressure, or from the vertical press position. The pads share one tab stop and are navigated with the arrow keys.

Props · 3

EventDetailDescription
padon{ value, velocity, reason }A pad is struck. preventDefault() keeps it silent
padoff{ value, reason }A pad is released; reason is "choke" when another pad of its group stopped it
PartDescription
<audio-ui-pad-item>A pad, a button labelled by its content. value identifies it in events, choke-group makes pads stop each other, and disabled silences it. Sets --pad-velocity (0 to 1) to the last strike's velocity

Piano

<audio-ui-piano> is a playable keyboard with role="group". A key sounds while pressed, by pointer or with Enter and Space. Dragging glides across keys, and each pointer holds its own key, so chords can be played on touch screens. The keys share one tab stop and are navigated with the arrow keys, by octave with Shift.

Props · 7

EventDetailDescription
noteon{ pitch, velocity, reason, event }A key starts sounding. event is the originating pointer or keyboard event. preventDefault() keeps it silent
noteoff{ pitch, reason, event }A key stops sounding
PartDescription
<audio-ui-piano-keys>Generates an <audio-ui-piano-key> per pitch, lowest first, from its <template>. Renders with display: contents
<audio-ui-piano-key>A key, a button positioned for its pitch and labelled with its name ("C4"). data-color is black or white
<audio-ui-piano-key-label>The name of its key, hidden from assistive technology

XY pad

<audio-ui-xypad> controls a two-dimensional value { x, y }. A press moves the point to the pointer and a drag follows it; the wheel, the trackpad and the arrow keys move it; a double click or Delete resets it. The position is exposed as --xy-pad-x (from the left) and --xy-pad-y (from the top), as percentages.

Props · 12

input and change carry { value: { x, y }, reason }.

PartDescription
<audio-ui-xypad-control>The interactive surface: press, drag, wheel, double click to reset
<audio-ui-xypad-thumb>Marks the point and contains one native range input per axis. Its aria-label labels both axes ("Filter X", "Filter Y"); label-x and label-y label each one
<audio-ui-xypad-value>Both axes, formatted as "x, y"
<audio-ui-xypad-label>Labels both inputs when the thumb has no accessible name

Meter

<audio-ui-meter> displays one or more level channels. Levels come from the value and peak attributes or properties, one per channel, or from a live source property, { getSnapshot, subscribe }, whose frames are written directly to the parts. A click clears clip indicators latched by a live source.

Props · 11

PartDescription
<audio-ui-meter-channels>Generates an <audio-ui-meter-channel> per level from its <template>. label names each channel, {n} being its number (Output {n}). Renders with display: contents
<audio-ui-meter-channel>A channel with role="meter", for its index. Carries data-warning, data-clipping, and data-clipped until cleared
<audio-ui-meter-track>The full range of a channel
<audio-ui-meter-indicator>The lit range, scaled to the level
<audio-ui-meter-peak>The peak-hold marker, hidden when the channel has none
<audio-ui-meter-value>The formatted level
<audio-ui-meter-label>Labels its channel
<audio-ui-meter-mark>A scale graduation at its value, such as -18

Sequencer

<audio-ui-sequencer> is a step grid with one row per sound and role="group". Pressing a cell toggles it, and dragging applies the new state to every cell crossed. The cells share one tab stop and are navigated with the arrow keys. Rows and steps are generated from templates:

<audio-ui-sequencer aria-label="Pattern" default-value="x...x... ..x...x.">
  <audio-ui-sequencer-rows labels="Kick, Snare">
    <template>
      <audio-ui-sequencer-steps>
        <template><audio-ui-sequencer-cell></audio-ui-sequencer-cell></template>
      </audio-ui-sequencer-steps>
    </template>
  </audio-ui-sequencer-rows>
</audio-ui-sequencer>

Props · 3

EventDetailDescription
input, change{ value, reason }The pattern changes, then the gesture ends
velocityinput, velocitychange{ velocities, reason }Step velocities change, then the gesture ends

The velocities property holds each step's velocity as number[][] (0 to 1). The source property, { getSnapshot, subscribe } returning the current step, marks that step's cells with data-current.

PartDescription
<audio-ui-sequencer-rows>Generates an <audio-ui-sequencer-row> per row from its <template>. labels names them, separated by commas
<audio-ui-sequencer-row>A row with role="group", for its index
<audio-ui-sequencer-steps>Generates an <audio-ui-sequencer-step> per step from its <template>. Both render with display: contents, so the cells are the grid items
<audio-ui-sequencer-cell>A step, a toggle button pressed when on (data-on), labelled with its row and step ("Kick, step 5")
<audio-ui-sequencer-velocity>A step's velocity as a bar sized by --sequencer-velocity. Press or drag to set it; Alt + click or Delete resets it to full; its native range input takes the keyboard

Envelope

<audio-ui-envelope> is a breakpoint envelope editor with role="group". Points are dragged to move, added with a double click on the graph, and removed with a double click, Alt + click or Delete. Each segment has a curve handle at its midpoint, and Alt + drag on the graph bends the segment under the pointer. The points share one tab stop: Page Up and Page Down (or Home and End) move between them, and the arrow keys move the focused point. Points and segment handles are generated, from a <template> when one is provided:

<audio-ui-envelope aria-label="Amp envelope" default-value="0 0, 0.2 1, 0.6 0.5, 1 0">
  <audio-ui-envelope-graph>
    <audio-ui-envelope-fill></audio-ui-envelope-fill>
    <audio-ui-envelope-line></audio-ui-envelope-line>
  </audio-ui-envelope-graph>
  <audio-ui-envelope-segments></audio-ui-envelope-segments>
  <audio-ui-envelope-points></audio-ui-envelope-points>
</audio-ui-envelope>

Props · 9

input and change carry { value, reason, index }, with reason one of drag, keyboard, add, remove or curve. The getPointValueText(point, index) and getCurveValueText(curve, index) properties customize aria-valuetext.

PartDescription
<audio-ui-envelope-graph>The drawing surface, filling the envelope. A double click adds a point; carries data-blocked briefly when adding is refused
<audio-ui-envelope-line>The curve through the points, in currentColor, with a constant stroke width set by stroke-width
<audio-ui-envelope-fill>The area under the curve, in currentColor
<audio-ui-envelope-points>Generates an <audio-ui-envelope-point> per point. label names them, with {n} and {count} placeholders (Point {n} of {count} by default)
<audio-ui-envelope-point>A slider on the point's level, positioned with left and bottom. Carries data-dragging, data-removable, and data-blocked briefly when removal is refused
<audio-ui-envelope-segments>Generates an <audio-ui-envelope-segment> per segment. label names them (Curve {n} by default)
<audio-ui-envelope-segment>A slider on the segment's curve, from -1 to 1, at its midpoint. Drag or Up and Down to bend; double click or Delete to straighten. Carries data-flat when the segment cannot bend

Plot

<audio-ui-plot> defines two axes and lays out everything drawn on them in axis units, for response curves, filter displays and transfer functions. Each drawing part renders its own SVG covering the plot, in currentColor, with a constant stroke width. Curves take their function and traces their live source as properties:

<audio-ui-plot min-x="20" max-x="20000" scale-x="log" min-y="-24" max-y="24">
  <audio-ui-plot-grid></audio-ui-plot-grid>
  <audio-ui-plot-line axis="y" value="0"></audio-ui-plot-line>
  <audio-ui-plot-curve></audio-ui-plot-curve>
  <audio-ui-plot-handle aria-label="Low" value="200 6"></audio-ui-plot-handle>
</audio-ui-plot>
<script type="module">
  document.querySelector("audio-ui-plot-curve").fn = (hz) => response(hz);
</script>

Props · 8

The valueAt(clientX, clientY) method converts a pointer position into axis units.

PartDescription
<audio-ui-plot-curve>y = fn(x) across the plot, from the fn property. fill-to closes it to a y value as an area; samples sets the initial sampling (256), refined adaptively at sharp bends
<audio-ui-plot-line>A line at value: vertical on the x axis (default), horizontal on the y axis
<audio-ui-plot-band>The range of one axis between from and to, spanning the other axis
<audio-ui-plot-trace>A live line from the source property, { getSnapshot, subscribe }, redrawn on each change. Supports fill-to; min and max set an independent value scale
<audio-ui-plot-grid>Grid lines and labels at round values. axis (x, y or both), x-ticks and y-ticks, x-count and y-count, labels (none or an axis), and a format(value, axis) property. Lines carry data-part="line", labels data-part="label", both data-axis, and the zero line data-zero
<audio-ui-plot-mark>A label at value on one axis, hidden from assistive technology
<audio-ui-plot-dot>A non-interactive point: a static value ("x y") or the live point of its source property
<audio-ui-plot-handle>A draggable point, with value and default-value as "x y", optionally constrained to one axis. Relative drag (Shift for fine), reset on double click, Alt + click or Delete. Contains a native range input per axis, labelled from its aria-label ("Low X") or label-x and label-y. Fires input and change with { value, reason }
<audio-ui-plot-cursor>Tracks a mouse or pen over the plot, setting --plot-cursor-x, --plot-cursor-y and data-active
<audio-ui-plot-cursor-readout>Inside the cursor, the value under the pointer as text, formatted by its format property

Waveform

<audio-ui-waveform> displays audio and, with a control part, seeks through it: a press seeks to the pointer, a drag scrubs, and the keyboard steps. The shape comes from the peaks property or a live levels source; the position from value or a live source, exposed as --waveform-position:

<audio-ui-waveform duration="102">
  <audio-ui-waveform-control aria-label="Seek">
    <audio-ui-waveform-canvas></audio-ui-waveform-canvas>
    <audio-ui-waveform-playhead></audio-ui-waveform-playhead>
  </audio-ui-waveform-control>
</audio-ui-waveform>
<script type="module">
  document.querySelector("audio-ui-waveform").peaks = peaks;
</script>

Props · 8

input and change carry { value, reason }. The root carries data-active while a live signal is audible.

PartDescription
<audio-ui-waveform-control>The seekable surface, containing the native range input. getAriaValueText(value, duration) customizes its announcement. With selectable, Shift + drag selects a range and fires selectioninput and selectioncommit with [start, end]
<audio-ui-waveform-canvas>Renders on a canvas aligned to device pixels. variant (mirror, bars, line, or scope for raw samples), bar-width, bar-gap, bar-radius, min-bar-height, gain, mirrored, idle, mode (static or scrolling), scroll-speed, line-width. Colors from --waveform-color and --waveform-progress-color
<audio-ui-waveform-bars>Generates count <audio-ui-waveform-bar> elements (24), each sized by --waveform-level, for CSS-styled visualizers. Played bars carry data-played. Also gain, idle, min-level, mirrored
<audio-ui-waveform-playhead>A marker at the position
<audio-ui-waveform-thumb>A handle centered on the position
<audio-ui-waveform-hover>Shown at the hovered position; its format(value, duration) property renders it as text
<audio-ui-waveform-marker>A cue at value, labelled by its content
<audio-ui-waveform-region>A loop or selection from value ("start end"). Draggable unless movable="false". Fires regioninput and regionchange with { value, edge, reason }
<audio-ui-waveform-region-handle>Resizes the region's edge (start or end) by drag or through its native range input

Timeline

<audio-ui-timeline> is the root of an arrangement: a ruler that sets the position, lanes of clips that move, trim and fade, loop ranges, and a viewport that scrolls and zooms. Units are arbitrary: seconds, beats or steps.

<audio-ui-timeline duration="32" snap-step="1">
  <audio-ui-timeline-viewport>
    <audio-ui-timeline-ruler aria-label="Position"></audio-ui-timeline-ruler>
    <audio-ui-timeline-track>
      <audio-ui-timeline-track-header>Drums</audio-ui-timeline-track-header>
      <audio-ui-timeline-lane seekable>
        <audio-ui-timeline-clip aria-label="Beat" start="4" length="8">
          <audio-ui-timeline-clip-header>Beat</audio-ui-timeline-clip-header>
        </audio-ui-timeline-clip>
        <audio-ui-timeline-playhead></audio-ui-timeline-playhead>
      </audio-ui-timeline-lane>
    </audio-ui-timeline-track>
  </audio-ui-timeline-viewport>
</audio-ui-timeline>

Props · 13

EventDetailDescription
input, change{ value, reason }The position changes, then the gesture ends
zoomchange{ value, reason }The zoom changes, already clamped. preventDefault() rejects it

Every lane sets --timeline-unit, the width of one unit; placed parts set --timeline-offset and --timeline-length.

PartDescription
<audio-ui-timeline-viewport>The scrolling container. Cmd/Ctrl + wheel and pinch zoom around the pointer
<audio-ui-timeline-ruler>The lane that sets the position, containing the native range input. With selectable, Shift + drag selects a range and fires selectioninput and selectioncommit
<audio-ui-timeline-lane>A lane for placed parts. seekable sets the position from a press on empty space; with selectable, a drag selects a range
<audio-ui-timeline-playhead>A marker at the position
<audio-ui-timeline-mark>A part placed at value, such as a bar number. With movable, it can be dragged and fires markinput and markchange
<audio-ui-timeline-range>A loop or selection, value as "start end". Draggable, resizable with an <audio-ui-timeline-range-handle> per edge, and with drawable, drawn on empty lane. Fires rangeinput and rangechange with { value, edge, reason }
<audio-ui-timeline-navigator>An overview bar. Its <audio-ui-timeline-navigator-window> scrolls, its <audio-ui-timeline-navigator-handle>s zoom, and a double click fits the whole timeline
<audio-ui-timeline-track>A row with a header and a lane, sized by height in pixels. An <audio-ui-timeline-track-resize-handle> changes it and fires heightinput and heightchange. <audio-ui-timeline-track-header> stays pinned while the lane scrolls
<audio-ui-timeline-clip>A region of a source: start, length, offset, fade-in, fade-out, fade-in-shape, fade-out-shape, source-length. A press selects it (clipselect, then the selected attribute) and sets the position; Delete fires clipremove. Fires clipinput and clipchange with { value, reason, target, lane, delta, copy }. Sets --clip-unit for its own parts
<audio-ui-timeline-clip-header>Moves the clip by drag, across lanes
<audio-ui-timeline-clip-handle>Trims the clip's edge, shifting the source offset rather than stretching
<audio-ui-timeline-clip-fade-handle>Sets the fade at its edge (in or out). Double click or Enter cycles its shape
<audio-ui-timeline-clip-fade>Renders the fade at its edge, shaded with --clip-fade-shade

Piano roll

<audio-ui-piano-roll> is a note editor placed inside an <audio-ui-timeline>, which provides its length, zoom, grid and position. Its entry point also registers the timeline and piano elements. Notes are set through the value property as { id, pitch, start, length, velocity?, chance?, muted? }[], and the selection through the selection property. Rows, notes and velocity bars are generated from them:

<audio-ui-timeline duration="16" snap-step="0.25">
  <audio-ui-piano-roll mode="draw">
    <audio-ui-piano-roll-keys>
      <audio-ui-piano aria-label="Keys"><audio-ui-piano-keys></audio-ui-piano-keys></audio-ui-piano>
    </audio-ui-piano-roll-keys>
    <audio-ui-timeline-viewport>
      <audio-ui-piano-roll-grid aria-label="Notes">
        <audio-ui-piano-roll-rows></audio-ui-piano-roll-rows>
        <audio-ui-piano-roll-notes></audio-ui-piano-roll-notes>
        <audio-ui-timeline-playhead></audio-ui-timeline-playhead>
      </audio-ui-piano-roll-grid>
    </audio-ui-timeline-viewport>
  </audio-ui-piano-roll>
</audio-ui-timeline>

Props · 9

EventDetailDescription
input, change{ value, reason }The notes change, then the gesture ends
noteselect{ selection, reason }The selection changes. preventDefault() rejects it
noteon, noteoff{ pitch, reason, event }A pitch starts or stops sounding. preventDefault() on noteon keeps it silent

The getNoteLabel(note) property sets each note's accessible name, "C4, 4 to 6" by default.

PartDescription
<audio-ui-piano-roll-grid>The focusable note surface, a timeline lane. Enter adds a note at the position, Escape clears the selection, Cmd/Ctrl+A selects all, and Cmd/Ctrl+D, C, X, V and Q duplicate, copy, cut, paste and quantize
<audio-ui-piano-roll-rows>Generates an <audio-ui-piano-roll-row> per pitch, highest first. Rows carry data-color, data-in-scale, data-octave and data-active
<audio-ui-piano-roll-notes>Generates an <audio-ui-piano-roll-note> per note in reading order, from its <template>. Each note is a toggle button positioned at its start and pitch, movable by pointer and keyboard, exposing --piano-roll-velocity and --piano-roll-chance
<audio-ui-piano-roll-note-handle>Inside a note, resizes it from its edge
<audio-ui-piano-roll-marquee>The selection rectangle during a marquee drag
<audio-ui-piano-roll-selection-span>The time range of the selection; an <audio-ui-piano-roll-stretch-handle> per edge stretches it
<audio-ui-piano-roll-velocity-lane>A lane of per-note bars, through <audio-ui-piano-roll-velocities>, for velocity or, with level="chance", probability. Each <audio-ui-piano-roll-velocity> is sized by --piano-roll-level and contains a native range input
<audio-ui-piano-roll-keys>Lays out the <audio-ui-piano> it contains on the roll's rows. Keys sound through the roll; Shift + press selects the notes of that row

Data attributes

Parts reflect the state of their control:

Data attributes · 9

data-disabled
Present when disabled.
data-dragging
Present while a pointer drags the control.
data-drag-mode
On knob parts, the drag-mode, for cursor styling.
data-orientation
On fader parts, horizontal or vertical.
data-active
On a pad or a piano key while it sounds.
data-on
On a sequencer cell or velocity bar whose step is on.
data-current
On the cells of the current step.
data-painting
On the sequencer and its parts during a paint gesture.
data-blocked
On an envelope graph or point, briefly, when an add or remove is refused.
Built by Ouest Labs, the source code is available on GitHub.
RSS