Regions plugin

Regions are visual overlays on the waveform that mark segments of audio. They can be dragged, resized, clicked, and played back independently. See the live demo to explore them interactively.


Setup

Install wavesurfer.js (if you haven’t already — see Getting started), then import and register the plugin:

import WaveSurfer from 'wavesurfer.js'
import RegionsPlugin from 'wavesurfer.js/dist/plugins/regions.js'

const ws = WaveSurfer.create({
  container: '#waveform',
  url: '/audio.mp3',
})

const regions = ws.registerPlugin(RegionsPlugin.create())

CDN / UMD:

<script src="https://unpkg.com/wavesurfer.js@7"></script>
<script src="https://unpkg.com/wavesurfer.js@7/dist/plugins/regions.js"></script>
<script>
  const ws = WaveSurfer.create({ container: '#waveform', url: '/audio.mp3' })
  const regions = ws.registerPlugin(WaveSurfer.RegionsPlugin.create())
</script>

RegionsPlugin.create() takes no options — the constructor argument is undefined.


Adding regions

Call regions.addRegion(options) after the plugin is registered. The full set of accepted options:

Option Type Default Description
start number Required. Start time in seconds.
end number same as start End time in seconds. When equal to start the region renders as a marker (a vertical line).
id string auto-generated Unique identifier for the region.
color string 'rgba(0, 0, 0, 0.1)' CSS color for the region background.
content string | HTMLElement Label text or arbitrary HTML element shown inside the region.
drag boolean true Allow dragging the region.
resize boolean true Allow resizing both handles.
resizeStart boolean true Allow resizing the left (start) handle.
resizeEnd boolean true Allow resizing the right (end) handle.
minLength number 0 Minimum region length in seconds (enforced during resize).
maxLength number Infinity Maximum region length in seconds (enforced during resize).
channelIdx number -1 Restrict the region to a single channel by index. -1 means full height.
contentEditable boolean false Make the content label editable in-browser.
ws.on('ready', () => {
  // Basic region
  const intro = regions.addRegion({
    start: 0,
    end: 10,
    color: 'rgba(255, 100, 0, 0.2)',
    content: 'Intro',
  })

  // Marker (zero-width, rendered as a vertical line)
  regions.addRegion({
    start: 30,
    color: 'rgba(0, 0, 200, 0.8)',
    content: 'Drop',
  })

  // Region with an HTML element as label
  const badge = document.createElement('span')
  badge.className = 'badge'
  badge.textContent = 'Chorus'
  regions.addRegion({ start: 60, end: 90, content: badge })
})

addRegion() returns the Region instance synchronously. However, wait for ready before calling it — see the pitfalls section below.


Drag-to-create

enableDragSelection(options) lets users draw new regions by clicking and dragging on empty waveform space. It accepts any RegionParams except start and end (those come from the drag gesture):

ws.on('ready', () => {
  const disableDrag = regions.enableDragSelection({
    color: 'rgba(0, 150, 255, 0.2)',
  })

  // Call disableDrag() later to stop accepting new drag-created regions
  // disableDrag()
})

The returned function tears down the drag listener when called. Newly drawn regions fire the same region-created event as programmatically added ones.


Styling regions

Color via option

Pass a CSS color string to color when creating:

regions.addRegion({ start: 5, end: 15, color: 'rgba(200, 0, 100, 0.25)' })

Updating color with setOptions

region.setOptions(options) accepts a subset of RegionParams:

region.setOptions({
  color: 'rgba(0, 200, 100, 0.3)',  // updates backgroundColor immediately
  start: 5,
  end: 20,
  drag: false,
  resize: true,
  resizeStart: false,
  resizeEnd: true,
  content: 'Updated label',
  id: 'my-region',
})

setOptions({ color }) works as expected in v7. It directly sets element.style.backgroundColor — however it only sets backgroundColor, not the border (which is used by markers). If you created a zero-width marker and call setOptions({ color }), the left-border color is not updated. For regions (non-markers) color updates take effect immediately.

Note that minLength, maxLength, channelIdx, and contentEditable are not accepted by setOptions — they can only be set at creation time via addRegion.

CSS: content and borders

Regions render with CSS part attributes you can target in a stylesheet:

/* The region overlay */
[part~="region"] {
  border-radius: 4px;
}

/* The label inside a region */
[part~="region-content"] {
  font-size: 12px;
  font-weight: bold;
  color: #333;
}

/* Resize handles */
[part~="region-handle"] {
  width: 4px;
}
[part~="region-handle-left"]  { border-left-color: red; }
[part~="region-handle-right"] { border-right-color: blue; }

/* Target a specific region by its id */
[part~="my-region"] {
  outline: 2px solid gold;
}

A marker element gets part="marker <id>" instead of part="region <id>".


Custom data

In wavesurfer.js v7 the RegionParams type has no built-in data field. There is no automatic property for attaching arbitrary metadata to a region. The idiomatic v7 approach is to hold your own side-channel map keyed on the region’s id:

const meta = new Map()

const region = regions.addRegion({ start: 10, end: 20, id: 'verse-1' })
meta.set(region.id, { label: 'Verse 1', color: '#f00', track: 2 })

// Retrieve later, e.g. in an event handler
regions.on('region-clicked', (region) => {
  const data = meta.get(region.id)
  console.log(data.label)
})

Because id defaults to a random string, set it explicitly to a stable value if you need to look things up later. Alternatively, since the region instance is a plain JavaScript object, you can attach properties directly — but that is not type-safe and may clash with future fields:

// Works today, but not type-safe
region._myData = { track: 2 }

Using meta.set(region.id, ...) is the safer pattern.


Events

Plugin events

Subscribe on the regions plugin instance:

Event Payload When
region-initialized (region: Region) A region object is constructed but not yet added to the DOM or the internal list. Fires for both addRegion() and drag-to-create.
region-created (region: Region) Region is added to the DOM and the internal list. For drag-to-create this fires on drag end.
region-update (region: Region, side?: 'start' | 'end') Fires continuously while a region is being dragged or resized.
region-updated (region: Region, side?: 'start' | 'end') Fires once when a drag or resize interaction ends.
region-removed (region: Region) Fires when a region is removed.
region-clicked (region: Region, e: MouseEvent) Fires on a single click on the region element.
region-double-clicked (region: Region, e: MouseEvent) Fires on double-click.
region-in (region: Region) Playback position enters the region (based on timeupdate).
region-out (region: Region) Playback position leaves the region.
region-content-changed (region: Region) The region’s content (label) changes.

Region instance events

Subscribe on a Region instance returned by addRegion():

Event Payload When
click (e: MouseEvent) Single click on this region.
dblclick (e: MouseEvent) Double-click.
over (e: MouseEvent) Mouse enters the region element.
leave (e: MouseEvent) Mouse leaves the region element.
update (side?: 'start' | 'end') Fires while dragging or resizing.
update-end (side?: 'start' | 'end') Fires when drag/resize finishes.
play (end?: number) Fires when region.play() is called.
remove [] Fires when the region is about to be removed.
content-changed [] Fires when setContent() is called.

Seeking on click

regions.on('region-clicked', (region, e) => {
  e.stopPropagation()       // prevent the waveform's own click-to-seek
  region.play(true)         // seek to region start and play to region end
})

Looping playback

region.play(stopAtEnd?)

region.play() seeks wavesurfer to the region’s start and begins playback.

  • region.play() — plays from start, does not automatically stop at end.
  • region.play(true) — plays from start and stops when playback reaches end.
  • region.play(false) — same as region.play().
const loop = regions.addRegion({ start: 10, end: 20, color: 'rgba(0,100,200,0.2)' })

// One-shot: play the region once and stop
loop.play(true)

Looping with region-out

The region-out plugin event fires each time playback leaves a region. Use it to loop continuously:

let looping = false

regions.on('region-out', (region) => {
  if (looping && region === activeRegion) {
    region.play(true)
  }
})

let activeRegion = null

regions.on('region-clicked', (region, e) => {
  e.stopPropagation()
  activeRegion = region
  looping = true
  region.play(true)
})

// Stop looping on waveform click
ws.on('interaction', () => {
  looping = false
  activeRegion = null
})

region-out is the correct hook for looping — the plugin drives it internally from timeupdate. Avoid adding your own timeupdate listener to detect region boundaries; you’ll duplicate logic the plugin already handles.

For a simpler approach when you only need to loop once and don’t need to track which region is active, see the Playback docs.


Removing and clearing

// Remove a single region
region.remove()

// Remove all regions
regions.clearRegions()

// Get the current list before clearing
const all = regions.getRegions()
all.forEach((r) => console.log(r.id, r.start, r.end))
regions.clearRegions()

Known pitfalls and safe patterns

Adding a region before ready can silently fail. addRegion() internally reads wavesurfer.getDuration(). If the audio hasn’t loaded yet, getDuration() returns 0 — positions get clamped to 0 and the region may not render at the right place. The plugin does set up a once('ready') fallback in this case, but relying on it leads to subtle bugs. Always wait for the ready event before calling addRegion().

// Safe
ws.on('ready', () => {
  regions.addRegion({ start: 10, end: 20 })
})

Don’t mutate inside region-created. Calling region.remove() synchronously inside a region-created handler can leave the DOM node in place because the region hasn’t finished being saved. If you need to reject a newly created region, defer the removal:

// Unsafe
regions.on('region-created', (region) => {
  region.remove()  // DOM node may linger
})

// Safe — defer by one tick
regions.on('region-created', (region) => {
  if (shouldReject(region)) {
    setTimeout(() => region.remove(), 0)
  }
})

clearRegions() under frequent redraws. If you call clearRegions() while regions are being drawn (e.g., while audio is still rendering or in a tight loop), regions added immediately after may not appear because the container hasn’t finished updating. Wait for a stable state (e.g., inside a requestAnimationFrame or after ready) before clearing and re-adding.

Subscriptions accumulate after clearRegions(). Each regions.on('region-created', ...) listener and each region.on(...) listener is still active after regions are cleared. If you clearRegions() and then add new regions in a loop, listeners from the previous set are not automatically removed. Store the unsubscribe functions returned by .on() and call them explicitly when you’re done:

const unsub = regions.on('region-updated', (region) => {
  // ...
})

// When you no longer need this listener:
unsub()