# Envelope

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

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

## 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-envelope {
    display: block;
    width: 100%;
    max-width: 28rem;
    height: calc(var(--spacing, 0.25rem) * 40);
    border-radius: calc(var(--radius, 0.625rem) - 2px);
    background: var(--muted, oklch(0.97 0 0));
  }

  audio-ui-envelope-fill {
    color: var(--primary, oklch(0.205 0 0));
    opacity: 0.15;
  }

  audio-ui-envelope-line {
    color: var(--primary, oklch(0.205 0 0));
    stroke-width: 2px;
  }

  audio-ui-envelope-point {
    box-sizing: border-box;
    width: calc(var(--spacing, 0.25rem) * 3);
    height: calc(var(--spacing, 0.25rem) * 3);
    border: 2px solid var(--primary, oklch(0.205 0 0));
    border-radius: var(--radius, 0.625rem);
    background: var(--background, oklch(1 0 0));
    cursor: grab;
    outline: none;
  }

  audio-ui-envelope-point:focus-visible,
  audio-ui-envelope-point[data-dragging] {
    background: var(--primary, oklch(0.205 0 0));
  }

  audio-ui-envelope-segment {
    width: calc(var(--spacing, 0.25rem) * 2);
    height: calc(var(--spacing, 0.25rem) * 2);
    border-radius: var(--radius, 0.625rem);
    background: var(--muted-foreground, oklch(0.556 0 0));
    opacity: 0.5;
    cursor: ns-resize;
    outline: none;
  }

  audio-ui-envelope-segment[data-flat] {
    display: none;
  }
</style>

<audio-ui-envelope aria-label="Amp envelope" default-value="0 0, 0.12 1, 0.35 0.6 0.4, 0.75 0.6, 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>

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

  const envelope = document.querySelector("audio-ui-envelope");
  envelope.addEventListener("change", (event) =>
    console.log(event.detail.value)
  );
</script>
```

*With Tailwind*

```html
<audio-ui-envelope class="block h-40 w-full max-w-md rounded-md bg-muted" aria-label="Amp envelope" default-value="0 0, 0.12 1, 0.35 0.6 0.4, 0.75 0.6, 1 0">
  <audio-ui-envelope-graph>
    <audio-ui-envelope-fill class="text-primary opacity-15"></audio-ui-envelope-fill>
    <audio-ui-envelope-line class="text-primary [stroke-width:2px]"></audio-ui-envelope-line>
  </audio-ui-envelope-graph>
  <audio-ui-envelope-segments class="*:size-2 *:cursor-ns-resize *:rounded-lg *:bg-muted-foreground *:opacity-50 *:outline-none *:data-flat:hidden"></audio-ui-envelope-segments>
  <audio-ui-envelope-points class="*:size-3 *:cursor-grab *:rounded-lg *:border-2 *:border-primary *:bg-background *:outline-none *:focus-visible:bg-primary *:data-dragging:bg-primary"></audio-ui-envelope-points>
</audio-ui-envelope>

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

  const envelope = document.querySelector("audio-ui-envelope");
  envelope.addEventListener("change", (event) =>
    console.log(event.detail.value)
  );
</script>
```

**Vue**

*With CSS*

```vue
<script setup lang="ts">
import "@audio-ui/elements/envelope";
</script>

<template>
  <audio-ui-envelope aria-label="Amp envelope" default-value="0 0, 0.12 1, 0.35 0.6 0.4, 0.75 0.6, 1 0" @change="(event) => console.log(event.detail.value)">
    <audio-ui-envelope-graph>
      <audio-ui-envelope-fill />
      <audio-ui-envelope-line />
    </audio-ui-envelope-graph>
    <audio-ui-envelope-segments />
    <audio-ui-envelope-points />
  </audio-ui-envelope>
</template>

<style>
audio-ui-envelope {
  display: block;
  width: 100%;
  max-width: 28rem;
  height: calc(var(--spacing, 0.25rem) * 40);
  border-radius: calc(var(--radius, 0.625rem) - 2px);
  background: var(--muted, oklch(0.97 0 0));
}

audio-ui-envelope-fill {
  color: var(--primary, oklch(0.205 0 0));
  opacity: 0.15;
}

audio-ui-envelope-line {
  color: var(--primary, oklch(0.205 0 0));
  stroke-width: 2px;
}

audio-ui-envelope-point {
  box-sizing: border-box;
  width: calc(var(--spacing, 0.25rem) * 3);
  height: calc(var(--spacing, 0.25rem) * 3);
  border: 2px solid var(--primary, oklch(0.205 0 0));
  border-radius: var(--radius, 0.625rem);
  background: var(--background, oklch(1 0 0));
  cursor: grab;
  outline: none;
}

audio-ui-envelope-point:focus-visible,
audio-ui-envelope-point[data-dragging] {
  background: var(--primary, oklch(0.205 0 0));
}

audio-ui-envelope-segment {
  width: calc(var(--spacing, 0.25rem) * 2);
  height: calc(var(--spacing, 0.25rem) * 2);
  border-radius: var(--radius, 0.625rem);
  background: var(--muted-foreground, oklch(0.556 0 0));
  opacity: 0.5;
  cursor: ns-resize;
  outline: none;
}

audio-ui-envelope-segment[data-flat] {
  display: none;
}
</style>
```

*With Tailwind*

```vue
<script setup lang="ts">
import "@audio-ui/elements/envelope";
</script>

<template>
  <audio-ui-envelope class="block h-40 w-full max-w-md rounded-md bg-muted" aria-label="Amp envelope" default-value="0 0, 0.12 1, 0.35 0.6 0.4, 0.75 0.6, 1 0" @change="(event) => console.log(event.detail.value)">
    <audio-ui-envelope-graph>
      <audio-ui-envelope-fill class="text-primary opacity-15" />
      <audio-ui-envelope-line class="text-primary [stroke-width:2px]" />
    </audio-ui-envelope-graph>
    <audio-ui-envelope-segments class="*:size-2 *:cursor-ns-resize *:rounded-lg *:bg-muted-foreground *:opacity-50 *:outline-none *:data-flat:hidden" />
    <audio-ui-envelope-points class="*:size-3 *:cursor-grab *:rounded-lg *:border-2 *:border-primary *:bg-background *:outline-none *:focus-visible:bg-primary *:data-dragging:bg-primary" />
  </audio-ui-envelope>
</template>
```

**Svelte**

*With CSS*

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

<audio-ui-envelope aria-label="Amp envelope" default-value="0 0, 0.12 1, 0.35 0.6 0.4, 0.75 0.6, 1 0" onchange={(event) => console.log(event.detail.value)}>
  <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>

<style>
:global {
  audio-ui-envelope {
    display: block;
    width: 100%;
    max-width: 28rem;
    height: calc(var(--spacing, 0.25rem) * 40);
    border-radius: calc(var(--radius, 0.625rem) - 2px);
    background: var(--muted, oklch(0.97 0 0));
  }

  audio-ui-envelope-fill {
    color: var(--primary, oklch(0.205 0 0));
    opacity: 0.15;
  }

  audio-ui-envelope-line {
    color: var(--primary, oklch(0.205 0 0));
    stroke-width: 2px;
  }

  audio-ui-envelope-point {
    box-sizing: border-box;
    width: calc(var(--spacing, 0.25rem) * 3);
    height: calc(var(--spacing, 0.25rem) * 3);
    border: 2px solid var(--primary, oklch(0.205 0 0));
    border-radius: var(--radius, 0.625rem);
    background: var(--background, oklch(1 0 0));
    cursor: grab;
    outline: none;
  }

  audio-ui-envelope-point:focus-visible,
  audio-ui-envelope-point[data-dragging] {
    background: var(--primary, oklch(0.205 0 0));
  }

  audio-ui-envelope-segment {
    width: calc(var(--spacing, 0.25rem) * 2);
    height: calc(var(--spacing, 0.25rem) * 2);
    border-radius: var(--radius, 0.625rem);
    background: var(--muted-foreground, oklch(0.556 0 0));
    opacity: 0.5;
    cursor: ns-resize;
    outline: none;
  }

  audio-ui-envelope-segment[data-flat] {
    display: none;
  }
}
</style>
```

*With Tailwind*

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

<audio-ui-envelope class="block h-40 w-full max-w-md rounded-md bg-muted" aria-label="Amp envelope" default-value="0 0, 0.12 1, 0.35 0.6 0.4, 0.75 0.6, 1 0" onchange={(event) => console.log(event.detail.value)}>
  <audio-ui-envelope-graph>
    <audio-ui-envelope-fill class="text-primary opacity-15"></audio-ui-envelope-fill>
    <audio-ui-envelope-line class="text-primary [stroke-width:2px]"></audio-ui-envelope-line>
  </audio-ui-envelope-graph>
  <audio-ui-envelope-segments class="*:size-2 *:cursor-ns-resize *:rounded-lg *:bg-muted-foreground *:opacity-50 *:outline-none *:data-flat:hidden"></audio-ui-envelope-segments>
  <audio-ui-envelope-points class="*:size-3 *:cursor-grab *:rounded-lg *:border-2 *:border-primary *:bg-background *:outline-none *:focus-visible:bg-primary *:data-dragging:bg-primary"></audio-ui-envelope-points>
</audio-ui-envelope>
```

**React**

In React, the same control is the [Envelope](/docs/react/envelope) 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/envelope";
```

| Export | Kind | What it is |
| --- | --- | --- |
| `ENVELOPE_TAGS` | const | The names the envelope's elements are registered under, frozen: a page cannot rename them by accident. |
| `EnvelopeChangeEvent` | type | Fired once an interaction that changed the points ends. |
| `EnvelopeElement` | class | The root of a breakpoint envelope editor, with `role="group"`. |
| `EnvelopeElementEventMap` | interface | The events an envelope fires, as `addEventListener` types them. |
| `EnvelopeEventDetail` | interface | What an envelope's `input` and `change` events carry, besides the envelope itself as their target. |
| `EnvelopeFillElement` | class | The area under the curve, in `currentColor`. |
| `EnvelopeGraphElement` | class | The drawing surface, filling the envelope (`position: absolute; inset: 0`) and containing the line and the fill. |
| `EnvelopeInputEvent` | type | Fired on every change. |
| `EnvelopeLineElement` | class | The curve through the points, in `currentColor`, with a stroke width that stays constant as the graph scales. |
| `EnvelopePointElement` | class | A draggable handle for the point at `index`, positioned with `left` and `bottom`. |
| `EnvelopePointsElement` | class | Generates an `<audio-ui-envelope-point>` per point, in `x` order, from its `<template>` when present. |
| `EnvelopeSegmentElement` | class | A curve handle at the midpoint of the segment that starts at point `index`, a `role="slider"` from -1 to 1. |
| `EnvelopeSegmentsElement` | class | Generates an `<audio-ui-envelope-segment>` per segment, from its `<template>` when present. |

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

**In React**

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

