Sakuga Back to Sakuga
Theme
Sound
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.

@anicanon/sakuga/uiv1.0Used onWeb · iOS · Android

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 (selected on the one in use).
Not for

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 from seed. bars fixes the count.
  • value of duration (seconds) puts the played part in the accent. It is one clipped layer, so playback sweeps through the bars linearly.
  • onSeek makes it the slider, named by label: 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"). Without onSeek it is a picture, and screen readers skip it.
  • selected marks the sound option in use: its bars take the accent on motion-fast.
  • size md (28px) or sm (20px); variant="media" on dark art.
  • Tokens: --waveform-bar, --waveform-played, --waveform-selected. AudioPlayer feeds its --audio-wave tokens in; Timeline its per-track wave tokens.
WaveformExample.jsxReact
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

peaks
number[]
Real loudness, 0–1, resampled to the bars that fit.
bars
number
A fixed bar count instead of fitting the width.
seed
number
Shapes the calm placeholder drawn without peaks.
value
number
Seconds; the played part takes the accent.
duration
number
size
'sm' | 'md'
variant
'default' | 'media'
selected
boolean
The sound option in use.
active
boolean
onSeek
(seconds: number) => void
Makes it the slider; called with seconds.
label
string
The slider's name, e.g. "Position in His voice".
className
string
style
CSSProperties
WaveformExample.swiftiOS · coming soon
WaveformExample.ktAndroid · coming soon

Each 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-height
its own value
28px
waveform-height-sm
its own value
20px
waveform-focus-outline-offset
focus-ring-offset
3px
waveform-bars-span-basis
line-strong
2px
waveform-bars-span-width
line-strong
2px
waveform-bars-span-min-height
its own value
4px
waveform-bars-span-radius
radius-full
9999px

Motion

waveform-bars-span-duration
motion-fast
160ms
waveform-bars-span-easing
ease-standard
cubic-bezier(0.2, 0, 0, 1)

Checked 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 onSeek it is decorative and hidden from screen readers. With onSeek it is a slider named by label (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.