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 fromstart, does not automatically stop atend.region.play(true)— plays fromstartand stops when playback reachesend.region.play(false)— same asregion.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()