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/reactFor 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.
Description
A 24-minute recording with four chapters that you can drag to seek, or clip.
Code
waveform-scrubber-demo.tsx, the Demo story. The component itself comes from the package.
Reference
Propspeaks, duration, value… (15)
Props
| Prop | Type | |
|---|---|---|
peaksRequired | readonly number[]Loudness samples from 0 to 1, spread evenly over the recording; resampled to the bars that fit. | – |
durationRequired | numberThe recording's length, in seconds. | – |
value | numberThe playhead, in seconds (controlled). | – |
defaultValue | numberdefault 0 | 0 |
onValueChange | (seconds: number) => void | – |
onValueCommitted | (seconds: number) => voidWhen a drag or key press ends: the moment to seek a real player. | – |
chapters | readonly WaveformChapter[]default [] | [] |
clip | readonly [number, number]A range to clip, in seconds (controlled): the handles show and the rest dims. | – |
onClipChange | (range: [number, number]) => void | – |
step | numberSeconds per arrow key (a clip's handles move by the second). default 5 | 5 |
bars | numberHow many bars. Default: as many as fit, one every 5px. | – |
label | stringThe playhead's accessible name. Default: the labels' "Seek". | – |
locale | stringDefault: the YoujProvider locale. | – |
disabled | boolean | – |
sound | booleanWith sound on, scrubbing across a chapter start plays 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
| Moment | Tokens | ||
|---|---|---|---|
| First viewthe wave rises from its midline as it’s revealed from the start | duration.deliberateease.enter560ms · 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.enter140ms · 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
stepseconds (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.