Jump to a component
Core · Media
Waveform
Waveform draws a sound as bars: a voice line, a take, a track in the cut, a generated sound option. It is the one drawing every sound in Sakuga uses: AudioPlayer is a play button, a Waveform and the time, and Timeline draws its audio, dialogue and music tracks with it.
Live, from @anicanon/sakuga 2.0.0
Sakuga for iOS and Android is coming
iOS
Coming soon
Waveform for iOS is in the roughs. The tokens it will read are the ones on this page, the same on every platform.
Android
Coming soon
Waveform for Android is in the roughs. The tokens it will read are the ones on this page, the same on every platform.
- Use it for
- A sound drawn as bars: a voice line, a take, a track in the cut, a generated sound option (
selectedon the one in use). - Not for
- A sound with its play button and timeAudioPlayer
- The cut's tracks laid out in timeTimeline
- The playhead of an episode or a shortScrubber
Guidelines
- Bars are 2px wide with 2px gaps and never thicker; as many as fit the width.
peaks(0–1) is the real loudness, resampled to those bars; without it a calm, even shape is drawn fromseed.barsfixes the count. valueofduration(seconds) puts the played part in the accent. It is one clipped layer, so playback sweeps through the bars linearly.onSeekmakes it the slider, named bylabel: click or drag to seek (the position stays under the pointer, never eased), arrow keys move a second, Home and End jump to the ends, and the value is spoken ("1 second of 4 seconds"). WithoutonSeekit is a picture, and screen readers skip it.selectedmarks the sound option in use: its bars take the accent onmotion-fast.sizemd(28px) orsm(20px);variant="media"on dark art.- Tokens:
--waveform-bar,--waveform-played,--waveform-selected. AudioPlayer feeds its--audio-wavetokens in; Timeline its per-track wave tokens.
import { useState } from 'react';
import { List, ListRow, Waveform } from '@anicanon/sakuga/ui';
export default function WaveformExample() {
const [value, setValue] = useState(1.4);
const [value2, setValue2] = useState(2);
return (
<>
<Waveform
duration={4}
value={value}
onSeek={setValue}
seed={5}
label="Position in Rain on glass"
/>
<List label="Sound options">
{[1, 2, 3].map((i) => (
<ListRow
key={i}
title={'Option ' + i}
sub={value2 === i ? 'In use · 0:04' : '0:04'}
href="#"
onClick={(e) => {
e.preventDefault();
setValue2(i);
}}
>
<div style={{ width: 180 }}>
<Waveform seed={i * 4} size="sm" selected={value2 === i} />
</div>
</ListRow>
))}
</List>
</>
);
}PropsWaveformProps
peaksnumber[]barsnumberseednumbervaluenumberdurationnumbersize'sm' | 'md'variant'default' | 'media'selectedbooleanactivebooleanonSeek(seconds: number) => voidlabelstringclassNamestringstyleCSSPropertiesEach token reads a role, and the role lands on a value in each theme. Change the role and every component that reads it follows.
Space and shape
waveform-heightwaveform-height-smwaveform-focus-outline-offsetfocus-ring-offsetwaveform-bars-span-basisline-strongwaveform-bars-span-widthline-strongwaveform-bars-span-min-heightwaveform-bars-span-radiusradius-fullMotion
waveform-bars-span-durationmotion-fastwaveform-bars-span-easingease-standardChecked automatically with axe-core 4.11.4 against WCAG 2.2 A and AA, on the live preview above, in light and dark, on 26 October 2026.
- Result
- No issues found, in light or in dark.
- By design
- Without
onSeekit is decorative and hidden from screen readers. WithonSeekit is a slider named bylabel(default "Position") that Tab reaches: arrows move it by a second, Home and End to either end, and its value is spoken as "12 seconds of 40 seconds". - Not tested
- The automated check doesn't test keyboard use, focus order or what a screen reader says. The lines above are the design system's own account of them.