# 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`](/docs/primitives): 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](#api-reference) has a live preview.

## Installation

```bash
npm install @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:

```js
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

**JavaScript**

```html
<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>
```

**Vue**

Declare the tags as custom elements to Vue's compiler, in `vite.config.js`:

```js
vue({
  template: {
    compilerOptions: { isCustomElement: (tag) => tag.startsWith("audio-ui-") },
  },
});
```

```vue
<script setup>
import "@audio-ui/elements/knob";
import { ref } from "vue";

const cutoff = ref(1000);
</script>

<template>
  <audio-ui-knob
    min="20"
    max="20000"
    scale="log"
    :value="cutoff"
    @input="cutoff = $event.detail.value"
  >
    <audio-ui-knob-label>Cutoff</audio-ui-knob-label>
    <audio-ui-knob-control>
      <audio-ui-knob-track />
      <audio-ui-knob-indicator />
      <audio-ui-knob-thumb />
    </audio-ui-knob-control>
    <audio-ui-knob-value />
  </audio-ui-knob>
</template>
```

**Svelte**

```svelte
<script>
  import "@audio-ui/elements/knob";

  let cutoff = $state(1000);
</script>

<audio-ui-knob
  min="20"
  max="20000"
  scale="log"
  value={cutoff}
  oninput={(event) => (cutoff = event.detail.value)}
>
  <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>
```

**React**

In React, use `@audio-ui/react`: the same parts as components, with typed props, a `render` prop and controlled state. React 19 renders the custom elements as well, as the previews on this page do, but the components integrate with it natively.

```tsx
import { Knob } from "@audio-ui/react";

const [cutoff, setCutoff] = useState(1000);

<Knob.Root max={20_000} min={20} onValueChange={setCutoff} scale="log" value={cutoff}>
  <Knob.Label>Cutoff</Knob.Label>
  <Knob.Control>
    <Knob.Track />
    <Knob.Indicator />
    <Knob.Thumb />
  </Knob.Control>
  <Knob.Value />
</Knob.Root>;
```

## Value and events

Every control follows the same contract:

| Member | Kind | Description |
| --- | --- | --- |
| `input` | Event | Fires 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. |
| `change` | Event | Fires once when an interaction that changed the value ends. Use it to commit: start a note, record an undo step, save. |
| `value` | Property | Reads 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. |
| `name` | Attribute | Submits 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:

```css
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.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `min` | `number` | `0` |  |
| `max` | `number` | `100` |  |
| `step` | `number` | `1` |  |
| `large-step` | `number` | `10` | Step for Page Up, Page Down and Shift + Arrow keys. |
| `value` | `number` |  | The current value. Mirrored by the `value` property. |
| `default-value` | `number` |  | The initial value, restored by a double tap, Alt + click or Delete. Defaults to the middle of the travel. |
| `anchor` | `number` |  | The value the indicator arc starts from, such as `0` on a -1 to 1 pan. Defaults to `min`. |
| `drag-mode` | `"circular" \| "revolution" \| "vertical"` | `"circular"` | How pointer movement maps to the value. |
| `scale` | `"linear" \| "log"` | `"linear"` | How the value maps onto the travel. The `scale` property also accepts a custom `{ toPosition, toValue }`. |
| `format` | `string` |  | `Intl.NumberFormat` options as JSON, for the value part and `aria-valuetext`. The `format` property accepts the options object. |
| `locale` | `string` |  | Defaults to the browser locale. |
| `name` | `string` |  | The form field name. |
| `disabled` | `boolean` |  |  |

| Part | Description |
| --- | --- |
| `<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.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `min` | `number` | `-60` |  |
| `max` | `number` | `6` |  |
| `step` | `number` | `1` |  |
| `large-step` | `number` | `10` | Step for Page Up, Page Down and Shift + Arrow keys. |
| `value` | `number` |  | The current value. Mirrored by the `value` property. |
| `default-value` | `number` |  | The initial value, restored by a double click, Alt + click or Delete. Defaults to the midpoint. |
| `anchor` | `number` |  | The value the indicator fills from, such as `0` on a -1 to 1 pan. Defaults to `min`. |
| `orientation` | `"horizontal" \| "vertical"` | `"vertical"` |  |
| `format` | `string` |  | `Intl.NumberFormat` options as JSON, for the value part and `aria-valuetext`. |
| `locale` | `string` |  | Defaults to the browser locale. |
| `name` | `string` |  | The form field name. |
| `disabled` | `boolean` |  |  |

| Part | Description |
| --- | --- |
| `<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.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `columns` | `number` | `4` | Pads per row, used by Up and Down. Match it in the layout. |
| `active-pads` | `string` |  | Pads lit externally, such as by a sequencer or MIDI input, separated by spaces. The `activePads` property accepts an array. |
| `disabled` | `boolean` |  |  |

| Event | Detail | Description |
| --- | --- | --- |
| `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 |

| Part | Description |
| --- | --- |
| `<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.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `min-pitch` | `number` | `48` | Lowest MIDI pitch, inclusive. 60 is middle C. |
| `max-pitch` | `number` | `72` | Highest MIDI pitch, inclusive. |
| `pitches` | `string` |  | Restricts the keys to these pitches, separated by spaces. The `pitches` property accepts an array. |
| `active-notes` | `string` |  | Pitches lit externally, such as by MIDI input. The `activeNotes` property accepts an array. |
| `layout` | `"keyboard" \| "even"` | `"keyboard"` | Key sizes: piano proportions, or equal shares as on a piano roll. |
| `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | Vertical places the low end at the bottom. |
| `disabled` | `boolean` |  |  |

| Event | Detail | Description |
| --- | --- | --- |
| `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 |

| Part | Description |
| --- | --- |
| `<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.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `min-x` | `number` | `0` |  |
| `max-x` | `number` | `100` |  |
| `min-y` | `number` | `0` |  |
| `max-y` | `number` | `100` |  |
| `step-x` | `number` | `1` |  |
| `step-y` | `number` | `1` |  |
| `value` | `string` |  | The point, as `"x y"`. The `value` property reads and sets it as `{ x, y }`. |
| `default-value` | `string` |  | The initial point, restored on reset. Defaults to the center. |
| `format` | `string` |  | `Intl.NumberFormat` options as JSON, applied to each axis. |
| `locale` | `string` |  | Defaults to the browser locale. |
| `name` | `string` |  | The inputs submit as `name.x` and `name.y`. |
| `disabled` | `boolean` |  |  |

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

| Part | Description |
| --- | --- |
| `<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.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `string` |  | One level per channel, such as `"-12 -6"`. The count sets the number of channels. |
| `peak` | `string` |  | One peak-hold level per channel. Hold time and decay are managed outside the meter. |
| `min` | `number` | `-60` |  |
| `max` | `number` | `6` |  |
| `scale` | `"linear" \| "iec"` | `"linear"` | How levels map onto the length. The `scale` property also accepts a custom increasing curve. |
| `clip-threshold` | `number` | `0` | Level at or above which a channel is clipping. |
| `warn-threshold` | `number` |  | Level at or above which a channel enters its warning zone. None by default. |
| `orientation` | `"horizontal" \| "vertical"` | `"vertical"` |  |
| `unit` | `string` |  | Appended to each channel's `aria-valuetext`, such as `dB`. The `getAriaValueText` property overrides the text. |
| `format` | `string` |  | `Intl.NumberFormat` options as JSON. One decimal by default. |
| `locale` | `string` |  | Defaults to the browser locale. |

| Part | Description |
| --- | --- |
| `<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:

```html
<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>
```

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `string` |  | The pattern as text: one word per row, one character per step, `x` for on and `.` for off. The `value` property reads and sets it as `boolean[][]`. |
| `default-value` | `string` |  | The initial pattern, in the same notation. |
| `disabled` | `boolean` |  |  |

| Event | Detail | Description |
| --- | --- | --- |
| `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`.

| Part | Description |
| --- | --- |
| `<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:

```html
<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>
```

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `string` |  | The points, separated by commas, each `x y` or `x y curve`: coordinates from 0 to 1, curve from -1 to 1. The `value` property reads and sets them as `{ x, y, curve? }[]`. |
| `default-value` | `string` |  | The initial points, in the same notation. Defaults to an ADSR shape. |
| `min-points` | `number` | `2` | Removal is refused at this count. |
| `max-points` | `number` |  | Adding is refused at this count. |
| `snap` | `string` | `0` | Pointer grid: one step, such as `0.25`, or `"x y"` per axis (`"0.125 0"` snaps time only). Cmd or Ctrl bypasses it. |
| `step` | `number` | `0.01` | Keyboard step on both axes. |
| `format` | `string` |  | `Intl.NumberFormat` options as JSON, for point coordinates. |
| `locale` | `string` |  | Defaults to the browser locale. |
| `disabled` | `boolean` |  |  |

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

| Part | Description |
| --- | --- |
| `<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:

```html
<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>
```

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `min-x` | `number` | `0` |  |
| `max-x` | `number` | `1` |  |
| `min-y` | `number` | `0` |  |
| `max-y` | `number` | `1` |  |
| `scale-x` | `"linear" \| "log"` | `"linear"` | The `scaleX` property also accepts a custom `{ toPosition, toValue }`. |
| `scale-y` | `"linear" \| "log"` | `"linear"` |  |
| `locale` | `string` |  | Locale of the handles' values. Defaults to the browser locale. |
| `disabled` | `boolean` |  |  |

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

| Part | Description |
| --- | --- |
| `<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`:

```html
<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>
```

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `duration` | `number` | `1` | Length of the audio, in the unit of the position, typically seconds. |
| `value` | `number` |  | The playback position, from 0 to `duration`. |
| `default-value` | `number` | `0` |  |
| `step` | `number` |  | Arrow key step. Defaults to `duration / 100`. |
| `large-step` | `number` |  | Page Up, Page Down and Shift + Arrow step. Defaults to `duration / 10`. |
| `range` | `string` |  | The visible window, `"start end"`, for clips and zoomed views. Defaults to the whole audio. Sets `data-outside` while the position is outside it. |
| `loading` | `boolean` |  | Seeking is disabled and the bars animate until the audio is available. |
| `disabled` | `boolean` |  |  |

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

| Part | Description |
| --- | --- |
| `<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.

```html
<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>
```

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `duration` | `number` | `1` | Length of the timeline, in its units. |
| `position` | `number` |  | The playback position. The `source` property follows a live position instead. |
| `default-position` | `number` | `0` |  |
| `zoom` | `number` |  | Pixels per unit. When unset, the timeline fits its width. |
| `default-zoom` | `number` |  |  |
| `min-zoom` | `number` | `0` |  |
| `max-zoom` | `number` |  |  |
| `snap-step` | `number` | `0` | Grid spacing. Gestures snap to it unless Cmd or Ctrl is held; 0 disables the grid. |
| `snap-offset` | `number` | `0` | Where the grid starts. |
| `snap-points` | `string` |  | Magnetic positions for dragged clips and ranges, separated by spaces, such as clip edges and locators. |
| `step` | `number` |  | Arrow key step. Defaults to `snap-step`, or `duration / 100`. |
| `large-step` | `number` |  | Page Up and Page Down step. Defaults to ten steps. |
| `disabled` | `boolean` |  |  |

| Event | Detail | Description |
| --- | --- | --- |
| `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`.

| Part | Description |
| --- | --- |
| `<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:

```html
<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>
```

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `min-pitch` | `number` | `48` | Lowest row, a MIDI pitch. 60 is middle C. |
| `max-pitch` | `number` | `72` | Highest row. |
| `folded` | `string` |  | When present, only pitches with notes get a row; `"scale"` also keeps the scale's pitches. |
| `scale` | `string` |  | Scale name, such as `major`, `minor` or `dorian`, with `scale-root` (0 for C). Its rows carry `data-in-scale`. |
| `mode` | `"select" \| "draw"` | `"select"` | Behavior of a press on empty grid: marquee selection or note drawing. |
| `draw-style` | `"stretch" \| "paint"` | `"stretch"` | In draw mode, `paint` draws a note in each grid cell crossed. |
| `min-length` | `number` |  | Minimum note length. Defaults to the timeline's grid step. |
| `active-pitches` | `string` |  | Pitches lit externally, separated by spaces, such as MIDI input or playing notes. |
| `disabled` | `boolean` |  |  |

| Event | Detail | Description |
| --- | --- | --- |
| `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.

| Part | Description |
| --- | --- |
| `<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**

| Attribute | Description |
| --- | --- |
| `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. |
