Waveform

Waveform visualizes audio data and can provide a seek control.

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> ); }
Scan to open this page on your phoneplocks.dev/ui/Waveform