Sakuga Back to Sakuga
Theme
Sound
Jump to a component

Core · Media

AudioPlayer

A voice line or sound clip: a round play button, a Waveform you can click, drag or arrow through to seek, and the time. Use it wherever a character's voice or an audio take is heard. Listening only: voice settings live in casting, never next to the player.

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

Live, from @anicanon/sakuga 2.0.0

Sakuga for iOS and Android is coming

iOS

Coming soon

AudioPlayer 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

AudioPlayer 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
Hearing a voice line or an audio take: a round play button, a waveform to seek through, and the time.
Not for

Guidelines

The waveform is the core Waveform: bars 2px wide with 2px gaps, never thicker, as many as fit. The played colour sweeps through them linearly as value advances; dragging seeks with no easing. The play button sinks on press and its icon swaps in.

  • label names the sound for screen readers when no title shows: the play button says "Play" and the waveform "Position in" with it.
AudioPlayerExample.jsxReact
import { useEffect, useRef, useState } from 'react';
import { AudioPlayer, Button } from '@anicanon/sakuga/ui';

function usePlay(start, len) {
  const [value, setValue] = useState(start);
  const [playing, setPlaying] = useState(false);
  const t = useRef(start);
  useEffect(() => {
    if (!playing) return undefined;
    let raf;
    let last = performance.now();
    function tick(now) {
      t.current = Math.min(len, t.current + (now - last) / 1000);
      last = now;
      setValue(t.current);
      if (t.current >= len) {
        setPlaying(false);
        return;
      }
      raf = requestAnimationFrame(tick);
    }
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [playing]);
  return {
    value: value,
    playing: playing,
    onToggle: (on) => {
      if (on && t.current >= len) {
        t.current = 0;
        setValue(0);
      }
      setPlaying(on);
    },
    onSeek: (s) => {
      t.current = s;
      setValue(s);
    },
  };
}

export default function AudioPlayerExample() {
  const a = usePlay(1.6, 4);
  const b = usePlay(0.8, 4);
  const c = usePlay(2.4, 6);
  return (
    <>
      <AudioPlayer
        {...Object.assign(
          {
            title: 'His voice',
            subtitle: 'Cast as Shounen Hero · warm, a little husky',
            duration: 4,
          },
          a,
        )}
      >
        <Button variant="ghost" size="sm" icon="mic">
          Recast
        </Button>
      </AudioPlayer>
      <AudioPlayer
        {...Object.assign({ size: 'sm', duration: 4, seed: 3 }, b)}
      />
      <img
        src="/art/eba-cinema.webp"
        alt=""
        style={{
          position: 'absolute',
          inset: 0,
          width: '100%',
          height: '100%',
          objectFit: 'cover',
        }}
      />
      <div
        style={{
          position: 'absolute',
          inset: 0,
          background:
            'linear-gradient(0deg,var(--bg-scrim-strong),transparent 85%)',
        }}
      />
      <AudioPlayer
        {...Object.assign(
          {
            variant: 'media',
            title: 'The theme',
            subtitle: 'Over the opening shot',
            duration: 6,
            seed: 5,
          },
          c,
        )}
      />
    </>
  );
}

PropsAudioPlayerProps

durationrequired
number
Played part of the waveform is the accent.
title
string
With a title the play button grows to 48 and the waveform sits under the titles.
subtitle
string
With a title the play button grows to 48 and the waveform sits under the titles.
label
string
Names the sound for screen readers when no title shows: "Play" and "Position in" are said with it.
value
number
Played part of the waveform is the accent.
playing
boolean
onToggle
(playing: boolean) => void
onSeek
(seconds: number) => void
peaks
number[]
Real loudness, resampled to the bars that fit; without it a calm, even shape is drawn from seed.
bars
number
seed
number
size
'sm' | 'md'
sm for cards and rows.
variant
'default' | 'media'
media on dark art: the play button is white with a dark glyph, as Button media is, so the one action reads over the picture.
className
string
style
CSSProperties
children
ReactNode
Trailing actions beside the title (Recast, a menu).
AudioPlayerExample.swiftiOS · coming soon
AudioPlayerExample.ktAndroid · coming soon

Tokens: --audio-play-bg, --audio-play-fg, --audio-wave, --audio-wave-played, --audio-time.

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.

Colour

audio-play-media-bgAudioPlayer: the play button over art, white as Button media is.
action-medianeutral-0#ffffff
action-medianeutral-0#ffffff
audio-play-media-fgAudioPlayer: the play glyph over art, dark on the white button.
text-on-action-medianeutral-925#0b0a10
text-on-action-medianeutral-925#0b0a10

Space and shape

audio-play-bg
action-primary
#ffffff
audio-play-fg
text-inverse
#0b0a10
audio-wave
its own value
color-mix(in srgb,#ffffff 26%,transparent)
audio-wave-played
accent
#f4779f
audio-time
text-muted
#a3a3a7
audio-title
text-primary
#ffffff
audio-sub
text-muted
#a3a3a7
audio-gap
gap-lg
12px
audio-play-size
its own value
40px
audio-play-active-scale
scale-press-sm
.94
audio-play-size-sm
its own value
32px
audio-titled-play-size
its own value
48px
audio-play-focus-outline-offset
focus-ring-offset
3px
audio-main-gap
gap-sm
6px
audio-head-gap
gap-lg
12px
audio-actions-gap
gap-xs
4px
audio-row-gap
gap-lg
12px
audio-play-icon-size-sm
icon-size-xs
14px
audio-play-icon-size
icon-size-sm
16px
audio-forced-edge-width
line-hairline
1px

Type

audio-font-family
font-sans
Funnel Sans
audio-title-font
text-label
600 14px/20px "Funnel Sans", system-ui, -apple-system, "Segoe UI", sans-serif
audio-sub-font
text-small
500 12px/16px "Funnel Sans", system-ui, -apple-system, "Segoe UI", sans-serif
audio-time-font
text-small-strong
600 12px/16px "Funnel Sans", system-ui, -apple-system, "Segoe UI", sans-serif

Motion

audio-play-duration
motion-fast
160ms
audio-play-easing
ease-standard
cubic-bezier(0.2, 0, 0, 1)
audio-play-active-duration
motion-instant
100ms
audio-play-icon-swap-animation
motion-swap
sk-swap 160ms cubic-bezier(0, 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
Tab reaches the play button, announced "Play" or "Pause" (from playing once it is given) followed by label, else title (or "audio"), then the title's actions (children), then, with onSeek, the waveform: a slider named "Position in" plus that name, where arrows move a second, Home and End jump to either end, and the value is spoken as "12 seconds of 40 seconds". The time ("0:12 / 0:40") reads as text; without onSeek the waveform is hidden and can't be moved.
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.