Skip to content

Waveform Scrubber

Navigate a recording’s waveform with chapters, keyboard controls and clip selection.

import { WaveformChapter, WaveformScrubber } from '@youj/react'

Installation & usage

npm i @youj/react

For an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.

Use a waveform scrubber as the seek bar of a podcast, a voice note, a lecture or a workout recording, anywhere the shape of the sound helps people find their place. The loudness samples you pass become bars: played in ink, the rest dim. Chapter starts are ticks with their titles above, a ruler of minutes runs underneath, and hovering shows the time and chapter under the pointer. Drag anywhere on the wave, or use the keys: a Base UI slider does the work underneath. With clip it becomes a range with two accent handles, zoomed to a window round the clip, for sharing a moment. It decodes no audio: bring the samples from your pipeline, or decode a file once and store them.

import { WaveformScrubber } from '@youj/react'

<WaveformScrubber
  // Loudness from 0 to 1, spread evenly over the recording.
  peaks={peaks}
  duration={audio.duration}
  value={time}
  onValueChange={setTime}
  onValueCommitted={(t) => (audio.currentTime = t)}
  chapters={[
    { start: 0, title: 'Cold open' },
    { start: 192, title: 'The brief' },
  ]}
/>

duration is the recording's length in seconds and value the playhead. onValueChange follows the drag frame by frame; onValueCommitted fires when a drag or a key press ends, the moment to seek a real player. The samples are resampled to the bars that fit (one every 5px, or bars of them), each bar taking the loudest sample in its slice so short peaks survive; there are never more bars than samples.

clip (a [start, end] range in seconds) turns the scrubber into two handles in the accent, with onClipChange reporting each move. While it clips, the wave zooms to a window round the range with at least a minute either side, and the handles step by the second. formatTimecode(seconds, locale) formats times the way the scrubber does (0:42, 12:34, 1:02:10), for the labels around it. The Podcast block puts it together with a player, a speed control and chapter rows.

Reference

Propspeaks, duration, value… (15)

Props

WaveformScrubber props
PropType
peaksRequiredreadonly number[]

Loudness samples from 0 to 1, spread evenly over the recording; resampled to the bars that fit.

–
durationRequirednumber

The recording's length, in seconds.

–
valuenumber

The playhead, in seconds (controlled).

–
defaultValuenumber

default 0

0
onValueChange(seconds: number) => void–
onValueCommitted(seconds: number) => void

When a drag or key press ends: the moment to seek a real player.

–
chaptersreadonly WaveformChapter[]

default []

[]
clipreadonly [number, number]

A range to clip, in seconds (controlled): the handles show and the rest dims. null for none.

–
onClipChange(range: [number, number]) => void–
stepnumber

Seconds per arrow key (a clip's handles move by the second).

default 5

5
barsnumber

How many bars. Default: as many as fit, one every 5px.

–
labelstring

The playhead's accessible name. Default: the labels' "Seek".

–
localestring

Default: the YoujProvider locale.

–
disabledboolean–
soundboolean

With sound on, scrubbing across a chapter start plays tick.

default true

true
Other props pass through: extends ComponentProps<'div'>.
StylingParts and data attributes

Styling

Parts: waveform-scrubber (with data-clipping), waveform-chapters, waveform-chapter (with data-current), waveform-slider, waveform-control, waveform-track, waveform-bars, waveform-chapter-tick, waveform-range, waveform-hover, waveform-value, waveform-playhead, waveform-handle (with data-kind), waveform-ruler and waveform-time. In Bezel the chapter titles are mono capital eyebrows and the times are mono. In a narrow scrubber only the chapter playing keeps its title.

MotionFirst view, scrub · reduced motion

Motion

Motion tokens
MomentTokens
First viewthe wave rises from its midline as it’s revealed from the start
duration.deliberateease.enter

560ms · cubic-bezier(0.16, 1, 0.3, 1)

560mscubic-bezier(0.16, 1, 0.3, 1)
Scrubthe playhead stays under the finger, never eased; its dot grows while held
duration.fastease.enter

140ms · cubic-bezier(0.16, 1, 0.3, 1)

140mscubic-bezier(0.16, 1, 0.3, 1)

Reduced motion

The wave is drawn in place.

Reduced motion

The wave is drawn in place from the first frame; scrubbing is unchanged, since it never animates.

AccessibilityKeyboard

Accessibility

  • The playhead is a slider named by label (default: the labels' "Seek"), whose value is spoken as the time, the length and the chapter ("12:34 of 42:10, Night shift").
  • Arrow keys step by step seconds (5), Page Up and Page Down ten times as far, and Home and End jump to the ends. A clip's handles, "Clip start" and "Clip end", step by the second.
  • With sound on, crossing into another chapter while scrubbing plays tick (sound={false} keeps it quiet).
  • Time runs left to right in every script, like a tape, so the wave keeps its direction under RTL, the arrow keys included; the text around it follows the page.
  • Its words come from the labels (waveformSeek, waveformPosition, waveformClipStart, waveformClipEnd), translated by the language packs.