# Waveform

> The <audio-ui-waveform> custom element of @audio-ui/elements: its attributes, its parts and a live preview.

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

## Usage

{/* usage:start: written by `bun scripts/element-usage.ts --write` */}

The styles read the theme's CSS variables, which [Styling](/docs/elements#styling) sets.

**JavaScript**

*With CSS*

```html
<style>
  audio-ui-waveform {
    display: block;
    width: 100%;
    max-width: 36rem;
  }

  audio-ui-waveform-control {
    position: relative;
    display: block;
    height: calc(var(--spacing, 0.25rem) * 20);
    border-radius: calc(var(--radius, 0.625rem) - 2px);
    background: var(--muted, oklch(0.97 0 0));
    cursor: pointer;
  }

  audio-ui-waveform-canvas {
    position: absolute;
    inset: 0;
    --waveform-progress-color: var(--primary, oklch(0.205 0 0));
    color: var(--muted-foreground, oklch(0.556 0 0));
  }

  audio-ui-waveform-region {
    top: 0;
    bottom: 0;
    background: color-mix(in oklab, var(--primary, oklch(0.205 0 0)) 10%, transparent);
  }

  audio-ui-waveform-region-handle {
    position: absolute;
    top: 0;
    bottom: 0;
    width: calc(var(--spacing, 0.25rem) * 1);
    background: var(--primary, oklch(0.205 0 0));
    cursor: ew-resize;
  }

  audio-ui-waveform-region-handle[edge="start"] {
    left: 0;
  }

  audio-ui-waveform-region-handle[edge="end"] {
    right: 0;
  }

  audio-ui-waveform-playhead {
    top: 0;
    bottom: 0;
    width: calc(var(--spacing, 0.25rem) * 0.5);
    background: var(--foreground, oklch(0.145 0 0));
  }

  audio-ui-waveform-hover {
    top: calc(var(--spacing, 0.25rem) * -6);
    translate: -50% 0;
    padding-inline: calc(var(--spacing, 0.25rem) * 1);
    border-radius: 0.25rem;
    background: var(--popover, oklch(1 0 0));
    font-family: var(--font-mono, ui-monospace, monospace);
    font-size: 10px;
    pointer-events: none;
  }
</style>

<audio-ui-waveform duration="96" value="30">
  <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">
  import "@audio-ui/elements/waveform";

  const waveform = document.querySelector("audio-ui-waveform");

  // Min and max pairs across the sound, from computePeaks in @audio-ui/engine/analysis.
  const peaks = [];
  waveform.peaks = peaks;
  waveform.addEventListener("input", (event) =>
    console.log(event.detail.value)
  );
</script>
```

*With Tailwind*

```html
<audio-ui-waveform class="block w-full max-w-xl" duration="96" value="30">
  <audio-ui-waveform-control class="relative block h-20 cursor-pointer rounded-md bg-muted" aria-label="Seek">
    <audio-ui-waveform-canvas class="absolute inset-0 text-muted-foreground [--waveform-progress-color:var(--primary)]"></audio-ui-waveform-canvas>
    <audio-ui-waveform-playhead class="inset-y-0 w-0.5 bg-foreground"></audio-ui-waveform-playhead>
  </audio-ui-waveform-control>
</audio-ui-waveform>

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

  const waveform = document.querySelector("audio-ui-waveform");

  // Min and max pairs across the sound, from computePeaks in @audio-ui/engine/analysis.
  const peaks = [];
  waveform.peaks = peaks;
  waveform.addEventListener("input", (event) =>
    console.log(event.detail.value)
  );
</script>
```

**Vue**

*With CSS*

```vue
<script setup lang="ts">
import "@audio-ui/elements/waveform";
import { ref } from "vue";

const position = ref(30);

// Min and max pairs across the sound, from computePeaks in @audio-ui/engine/analysis.
const peaks: number[] = [];
</script>

<template>
  <audio-ui-waveform duration="96" :peaks="peaks" v-model="position">
    <audio-ui-waveform-control aria-label="Seek">
      <audio-ui-waveform-canvas />
      <audio-ui-waveform-playhead />
    </audio-ui-waveform-control>
  </audio-ui-waveform>
</template>

<style>
audio-ui-waveform {
  display: block;
  width: 100%;
  max-width: 36rem;
}

audio-ui-waveform-control {
  position: relative;
  display: block;
  height: calc(var(--spacing, 0.25rem) * 20);
  border-radius: calc(var(--radius, 0.625rem) - 2px);
  background: var(--muted, oklch(0.97 0 0));
  cursor: pointer;
}

audio-ui-waveform-canvas {
  position: absolute;
  inset: 0;
  --waveform-progress-color: var(--primary, oklch(0.205 0 0));
  color: var(--muted-foreground, oklch(0.556 0 0));
}

audio-ui-waveform-region {
  top: 0;
  bottom: 0;
  background: color-mix(in oklab, var(--primary, oklch(0.205 0 0)) 10%, transparent);
}

audio-ui-waveform-region-handle {
  position: absolute;
  top: 0;
  bottom: 0;
  width: calc(var(--spacing, 0.25rem) * 1);
  background: var(--primary, oklch(0.205 0 0));
  cursor: ew-resize;
}

audio-ui-waveform-region-handle[edge="start"] {
  left: 0;
}

audio-ui-waveform-region-handle[edge="end"] {
  right: 0;
}

audio-ui-waveform-playhead {
  top: 0;
  bottom: 0;
  width: calc(var(--spacing, 0.25rem) * 0.5);
  background: var(--foreground, oklch(0.145 0 0));
}

audio-ui-waveform-hover {
  top: calc(var(--spacing, 0.25rem) * -6);
  translate: -50% 0;
  padding-inline: calc(var(--spacing, 0.25rem) * 1);
  border-radius: 0.25rem;
  background: var(--popover, oklch(1 0 0));
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: 10px;
  pointer-events: none;
}
</style>
```

*With Tailwind*

```vue
<script setup lang="ts">
import "@audio-ui/elements/waveform";
import { ref } from "vue";

const position = ref(30);

// Min and max pairs across the sound, from computePeaks in @audio-ui/engine/analysis.
const peaks: number[] = [];
</script>

<template>
  <audio-ui-waveform class="block w-full max-w-xl" duration="96" :peaks="peaks" v-model="position">
    <audio-ui-waveform-control class="relative block h-20 cursor-pointer rounded-md bg-muted" aria-label="Seek">
      <audio-ui-waveform-canvas class="absolute inset-0 text-muted-foreground [--waveform-progress-color:var(--primary)]" />
      <audio-ui-waveform-playhead class="inset-y-0 w-0.5 bg-foreground" />
    </audio-ui-waveform-control>
  </audio-ui-waveform>
</template>
```

**Svelte**

*With CSS*

```svelte
<script lang="ts">
  import "@audio-ui/elements/waveform";

  let position = $state(30);

  // Min and max pairs across the sound, from computePeaks in @audio-ui/engine/analysis.
  const peaks: number[] = [];
</script>

<audio-ui-waveform duration="96" peaks={peaks} value={position} oninput={(event) => (position = event.detail.value)}>
  <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>

<style>
:global {
  audio-ui-waveform {
    display: block;
    width: 100%;
    max-width: 36rem;
  }

  audio-ui-waveform-control {
    position: relative;
    display: block;
    height: calc(var(--spacing, 0.25rem) * 20);
    border-radius: calc(var(--radius, 0.625rem) - 2px);
    background: var(--muted, oklch(0.97 0 0));
    cursor: pointer;
  }

  audio-ui-waveform-canvas {
    position: absolute;
    inset: 0;
    --waveform-progress-color: var(--primary, oklch(0.205 0 0));
    color: var(--muted-foreground, oklch(0.556 0 0));
  }

  audio-ui-waveform-region {
    top: 0;
    bottom: 0;
    background: color-mix(in oklab, var(--primary, oklch(0.205 0 0)) 10%, transparent);
  }

  audio-ui-waveform-region-handle {
    position: absolute;
    top: 0;
    bottom: 0;
    width: calc(var(--spacing, 0.25rem) * 1);
    background: var(--primary, oklch(0.205 0 0));
    cursor: ew-resize;
  }

  audio-ui-waveform-region-handle[edge="start"] {
    left: 0;
  }

  audio-ui-waveform-region-handle[edge="end"] {
    right: 0;
  }

  audio-ui-waveform-playhead {
    top: 0;
    bottom: 0;
    width: calc(var(--spacing, 0.25rem) * 0.5);
    background: var(--foreground, oklch(0.145 0 0));
  }

  audio-ui-waveform-hover {
    top: calc(var(--spacing, 0.25rem) * -6);
    translate: -50% 0;
    padding-inline: calc(var(--spacing, 0.25rem) * 1);
    border-radius: 0.25rem;
    background: var(--popover, oklch(1 0 0));
    font-family: var(--font-mono, ui-monospace, monospace);
    font-size: 10px;
    pointer-events: none;
  }
}
</style>
```

*With Tailwind*

```svelte
<script lang="ts">
  import "@audio-ui/elements/waveform";

  let position = $state(30);

  // Min and max pairs across the sound, from computePeaks in @audio-ui/engine/analysis.
  const peaks: number[] = [];
</script>

<audio-ui-waveform class="block w-full max-w-xl" duration="96" peaks={peaks} value={position} oninput={(event) => (position = event.detail.value)}>
  <audio-ui-waveform-control class="relative block h-20 cursor-pointer rounded-md bg-muted" aria-label="Seek">
    <audio-ui-waveform-canvas class="absolute inset-0 text-muted-foreground [--waveform-progress-color:var(--primary)]"></audio-ui-waveform-canvas>
    <audio-ui-waveform-playhead class="inset-y-0 w-0.5 bg-foreground"></audio-ui-waveform-playhead>
  </audio-ui-waveform-control>
</audio-ui-waveform>
```

**React**

In React, the same control is the [Waveform](/docs/react/waveform) primitive of `@audio-ui/react`, with the same parts as React components and its value as props.

{/* usage:end */}

## API reference

```bash
npm install @audio-ui/elements
```

```ts
import "@audio-ui/elements/waveform";
```

| Export | Kind | What it is |
| --- | --- | --- |
| `WAVEFORM_TAGS` | const | The names the waveform's elements are registered under, frozen: a page cannot rename them by accident. |
| `WaveformBarElement` | class | A bar of `<audio-ui-waveform-bars>`, for its `index`. |
| `WaveformBarsElement` | class | Renders the waveform as `count` `<audio-ui-waveform-bar>` elements (24) for CSS-styled visualizers. |
| `WaveformCanvasElement` | class | Renders the waveform on a canvas aligned to the device pixel grid, so it stays sharp at any density. |
| `WaveformChangeEvent` | type | Fired once a seek that moved the position ends. |
| `WaveformControlElement` | class | The seekable surface: a press seeks to the pointer and a drag scrubs. |
| `WaveformControlElementEventMap` | interface | The events a `selectable` waveform control fires, as `addEventListener` types them. |
| `WaveformDescribe` | type | Returns how a position is announced. |
| `WaveformElement` | class | The root of a waveform. |
| `WaveformElementEventMap` | interface | The events a waveform fires, as `addEventListener` types them. |
| `WaveformEventDetail` | interface | What a waveform's `input` and `change` events carry, besides the waveform itself as their target. |
| `WaveformHoverElement` | class | Shown at the hovered position while the pointer is over the control. |
| `WaveformInputEvent` | type | Fired on every seek. |
| `WaveformLevelsSource` | type | A live signal: the current levels, 0…1 each, or raw samples for the `scope` variant. |
| `WaveformMarkerElement` | class | A cue or chapter marker at `value`, labelled by its content. |
| `WaveformPlayheadElement` | class | A non-interactive marker at the playback position. |
| `WaveformPositionSource` | type | A live playback position, in `duration` units. |
| `WaveformRangeDetail` | interface | What a selection's and a region's events carry: the stretch, `[start, end]` in `duration` units. |
| `WaveformRegionElement` | class | A loop or selection over the waveform, from `value` (`"start end"` in `duration` units; the whole audio by default). |
| `WaveformRegionElementEventMap` | interface | The events a region fires, as `addEventListener` types them. |
| `WaveformRegionHandleElement` | class | Resizes the `edge` (`start` or `end`) of its region by drag or through the native `<input type="range">` it contains, which receives its `aria-label`. |
| `WaveformThumbElement` | class | A handle centered on the playback position, through `--waveform-position`. |

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

**In React**

The same control in React, and its parts' props: [Waveform](/docs/react/waveform).

