Waveform
Waveform visualizes audio data and can provide a seek control.
Source
LLM docs
Package
Examples
7
Properties
64
Playground
Basics
Render a static waveform by passing the
peaks array and update playback UI by adjusting the progress prop. Here a Slider drives that value directly — both use fullWidth so they share the container's width.Loading demo…
import { useState } from 'react';
import { Block, Slider, Waveform } from '@plocks/ui';
import { WAVEFORM_DEMO_PEAKS } from '../data';
export function Demo() {
const [progress, setProgress] = useState<number>(0.3);
return (
<Block fullWidth>
<Waveform peaks={WAVEFORM_DEMO_PEAKS} progress={progress} h={64} fullWidth />
<Slider
value={Math.round(progress * 100)}
onChange={(percent) => setProgress(percent / 100)}
min={0}
max={100}
step={1}
valueLabel={(percent) => `${percent}%`}
/>
</Block>
);
}Full Width
A waveform is a fixed
w wide (300 by default); enable the fullWidth prop to let it stretch to its container instead.Loading demo…
import { Block, Text, Waveform } from '@plocks/ui';
import { WAVEFORM_DEMO_PEAKS } from '../data';
export function Demo() {
return (
<Block fullWidth>
<Block>
<Text variant="small">Default</Text>
<Waveform peaks={WAVEFORM_DEMO_PEAKS} progress={0.35} h={56} />
</Block>
<Block>
<Text variant="small">fullWidth</Text>
<Waveform peaks={WAVEFORM_DEMO_PEAKS} progress={0.6} h={56} fullWidth />
</Block>
</Block>
);
}Variants
variant picks the render style — bars (the default), rounded, line, or gradient, whose ramp derives from color unless you pass gradientColors. color and normalize restyle the result without changing the peak data.Loading demo…
import { Block, Text, Waveform } from '@plocks/ui';
import { QUIET_WAVEFORM_PEAKS, WAVEFORM_DEMO_PEAKS } from '../data';
const VARIANTS = ['bars', 'rounded', 'line', 'gradient'] as const;
export function Demo() {
return (
<Block fullWidth gap="lg">
<Block>
{VARIANTS.map((variant) => (
<Block key={variant} gap="xs">
<Text variant="small">{variant}</Text>
<Waveform peaks={WAVEFORM_DEMO_PEAKS} h={64} progress={0.4} variant={variant} fullWidth />
</Block>
))}
</Block>
<Block>
<Waveform peaks={WAVEFORM_DEMO_PEAKS} h={56} progress={0.25} color="primary" fullWidth />
<Waveform peaks={WAVEFORM_DEMO_PEAKS} h={56} progress={0.5} color="success" fullWidth />
<Waveform peaks={WAVEFORM_DEMO_PEAKS} h={56} progress={0.75} color="warning" fullWidth />
</Block>
<Block>
<Block gap="xs">
<Text variant="small">Default</Text>
<Waveform peaks={QUIET_WAVEFORM_PEAKS} h={56} progress={0.45} color="surface" fullWidth />
</Block>
<Block gap="xs">
<Text variant="small">normalize</Text>
<Waveform peaks={QUIET_WAVEFORM_PEAKS} h={56} progress={0.45} normalize color="surface" fullWidth />
</Block>
</Block>
</Block>
);
}Synchronized
Drive multiple waveforms from the same
progress state so scrubbing either track keeps every timeline aligned.Loading demo…
import { useState } from 'react';
import { Block, Text, Waveform } from '@plocks/ui';
import { TRACK_TWO_PEAKS, WAVEFORM_DEMO_PEAKS } from '../data';
export function Demo() {
const [progress, setProgress] = useState<number>(0.35);
return (
<Block fullWidth>
<Block>
<Text variant="small">Narration track</Text>
<Waveform
peaks={WAVEFORM_DEMO_PEAKS}
progress={progress}
h={80}
fullWidth
interactive
onSeek={setProgress}
/>
</Block>
<Block>
<Text variant="small">Background score</Text>
<Waveform
peaks={TRACK_TWO_PEAKS}
progress={progress}
h={80}
fullWidth
color="secondary"
interactive
onSeek={setProgress}
/>
</Block>
</Block>
);
}Audio playback
Drive the waveform from real playback:
expo-audio reports the position of a bundled clip, progress follows it, and onSeek scrubs by calling seekTo. The peaks array was measured from the same file, so the bars match what you hear.Loading demo…
import { useAudioPlayer, useAudioPlayerStatus } from 'expo-audio';
import { Block, Button, Waveform } from '@plocks/ui';
import { MELODY_PEAKS } from '../data';
const MELODY_SOURCE = require('../../../../assets/sounds/melody.mp3');
export function Demo() {
const player = useAudioPlayer(MELODY_SOURCE);
const status = useAudioPlayerStatus(player);
const duration = status.duration || 0;
const progress = duration > 0 ? Math.min(1, status.currentTime / duration) : 0;
const togglePlayback = () => {
if (status.playing) {
player.pause();
return;
}
// Replay from the top instead of sitting at the end of a finished clip.
if (status.didJustFinish || (duration > 0 && status.currentTime >= duration - 0.05)) {
player.seekTo(0);
}
player.play();
};
const handleSeek = (position: number) => {
if (duration > 0) {
player.seekTo(position * duration);
}
};
return (
<Block fullWidth>
<Waveform
peaks={MELODY_PEAKS}
progress={progress}
h={96}
fullWidth
interactive
onSeek={handleSeek}
showProgressLine
showTimeStamps
duration={duration}
accessibilityLabel="Arpeggio waveform"
accessibilityHint="Tap or drag to seek within the clip"
/>
<Button size="sm" onPress={togglePlayback}>
{status.playing ? 'Pause' : 'Play'}
</Button>
</Block>
);
}Synced to video
Use the waveform as a scrub bar for a
Video: onTimeUpdate feeds progress, and seeking on the waveform calls seek() on the video ref, so the two stay aligned in both directions.Loading demo…
import { useRef, useState } from 'react';
import { Asset } from 'expo-asset';
import { Block, Waveform } from '@plocks/ui';
import { Video } from '@plocks/media';
import type { VideoRef, VideoState } from '@plocks/media';
import { CLIP_PEAKS } from '../data';
// `Video` takes a URL, so resolve the bundled clip to one. `Image.resolveAssetSource`
// is native-only, whereas expo-asset works on web too.
const CLIP_URL = Asset.fromModule(require('../../../../assets/video/demo-clip.mp4')).uri;
export function Demo() {
const videoRef = useRef<VideoRef>(null);
const [playback, setPlayback] = useState({ currentTime: 0, duration: 0 });
const progress = playback.duration > 0 ? Math.min(1, playback.currentTime / playback.duration) : 0;
// `duration` is 0 until metadata loads, so keep the last known value.
const track = (state: VideoState) =>
setPlayback((prev) => ({
currentTime: state.currentTime,
duration: state.duration || prev.duration,
}));
return (
<Block w="100%" maw={640}>
<Video
ref={videoRef}
source={{ url: CLIP_URL }}
w="100%"
aspectRatio={640 / 426}
controls
onLoad={track}
onTimeUpdate={track}
onDurationChange={(duration) => setPlayback((prev) => ({ ...prev, duration }))}
/>
<Waveform
peaks={CLIP_PEAKS}
progress={progress}
h={72}
fullWidth
interactive
onSeek={(position) => videoRef.current?.seek(position * playback.duration)}
showProgressLine
showTimeStamps
duration={playback.duration}
accessibilityLabel="Waveform of the clip audio"
accessibilityHint="Tap or drag to seek the video"
/>
</Block>
);
}Interactive
Turn on the
interactive prop and handle onSeek/drag callbacks to update shared playback progress as the user scrubs the waveform.Loading demo…
import { useState } from 'react';
import { Block, Text, Waveform } from '@plocks/ui';
import { WAVEFORM_DEMO_PEAKS } from '../data';
export function Demo() {
const [progress, setProgress] = useState<number>(0.2);
return (
<Block fullWidth>
<Waveform
peaks={WAVEFORM_DEMO_PEAKS}
progress={progress}
fullWidth
h={72}
interactive
showProgressLine
onSeek={setProgress}
onDrag={setProgress}
accessibilityLabel="Audio timeline"
accessibilityHint="Drag or tap to seek"
/>
<Text variant="small">Progress: {Math.round(progress * 100)}%</Text>
</Block>
);
}React Native design for iOS, Android, and Web.
Quick Links
Resources