Audio player

Audio player plays a podcast episode from the bottom edge of the app: a compact bar that grows upward for speed, skips and a scrubbable waveform. Use it with episode cards on the Podcast tab.

  • Beta
  • Accessibility
  • Figma Control center 604:35881 · Episode cards 594:20678
  • packages/ui/src/components/AudioPlayer

Beta until design confirms the code-only hover and focus styles, the speed list, the slider keyboard steps and the narrow layout, and the player is tested with VoiceOver, NVDA and TalkBack on a real episode.

Preview

Change the controls to try every supported option. The docs ship no audio files, so this preview simulates playback with props; in an app you pass src.

Audio playerIntroduction to Design Principles

Introduction to Design Principles

The Design of Everyday Things · Episode 2

Introduction to Design Principles

The Design of Everyday Things · Episode 2

tsx
import { AudioPlayer } from '@nuton/ui'

<AudioPlayer
  title="Introduction to Design Principles"
  src="/podcasts/design-principles-1.mp3"
  subtitle="The Design of Everyday Things · Episode 2"
  onClose={closePlayer}
/>

Installation

Availability

@nuton/ui and @nuton/tokens are private workspace packages. They aren't published to a registry yet (D4: registry deferred; D12: distribution waits on the provenance review). Inside the Nuton monorepo, add them as workspace dependencies. The commands below apply once the packages are published.

1. Add the packages

bash
pnpm add @nuton/ui @nuton/tokens

Peer dependencies: react and react-dom 18 or later. The icons come from @remixicon/react, a dependency of @nuton/ui. The speed menu uses the Dropdown menu (Radix).

2. Load tokens and component styles once

tsx
// app/layout.tsx (or your app's entry file)
import '@nuton/tokens/tokens.css' // --nu-* design tokens
import '@nuton/ui/styles.css'     // component styles (uses the tokens)

Usage

tsx
import { AudioPlayer } from '@nuton/ui'

export function EpisodePlayer({ episode, onClose }) {
  return (
    <AudioPlayer
      title={episode.title}
      src={episode.audioUrl}
      onClose={onClose}
    />
  )
}
  • Pass src and the player renders its own <audio> element and reads time, duration and state from it. Use audioRef to reach the element.
  • Without src, drive it with props: playing, currentTime, duration, and the onPlayingChange, onSeek and onPlaybackRateChange callbacks. Use this for a shared or native player.
  • Show one player at a time, fixed near the bottom of the Podcast tab, and keep it open while people browse episodes.
  • Use EpisodeCard inside an EpisodeList for the episodes. Each card's buttons include the episode title in their names.
  • Pass onClose to show the close button. Stop playback when the player closes.

Variants and sizes

The dock is the player. The earlier floating pill remains as layout="pill" for existing uses: it has two surfaces, and Figma names them after the page theme (Property 1=Light is the dark pill) while code names them after the pill itself.

Appearance

Dock (the default)

The dock is the player (layout="dock" is the default): it is the bar for the bottom edge of an app. It follows the theme (light, dark or the device setting): full width, square corners, always the near-black surface, and a thin line along its top. It is compact by default (cover, title, waveform, time, play, expand, close) and grows upward when expanded (the waveform full width with both times, speed, back and forward, play, next, minimize). It is one player with two layers of controls, so expanding and minimizing never interrupt playback. Put it where it docks (a flex child under the content, not an overlay) and the content above it simply gets shorter. The waveform is the seek bar: pointer, arrow, Page and Home/End keys, and a spoken value. Pass peaks for a real waveform; without it a shape that suggests speech is drawn.

Audio player, compact exampleAffordances and signifiers

Affordances and signifiers

The Design of Everyday Things · Episode 2

Affordances and signifiers

The Design of Everyday Things · Episode 2

Audio player, expanded exampleAffordances and signifiers

Affordances and signifiers

The Design of Everyday Things · Episode 2

Affordances and signifiers

The Design of Everyday Things · Episode 2

Pill (earlier design, kept for existing uses)

The floating pill with the two surfaces below is the earlier design. New work uses the dock; the pill stays as layout="pill" only so existing screens keep working.

Audio player

Introduction to Design Principles

Audio player

Introduction to Design Principles

Episode card

  1. 1. Introduction to Design Principles

    The core principles behind products, systems and experiences.

    Download

Sizes

PartSizeToken or source
Player pillPadding 17 / 40, gap 24, full radius, 1px border, large custom shadow. About 76 tall.--nu-audio-player-pad-*, --nu-space-6, --nu-shadow-custom-shadows-large
Speed button40 tall, at least 40 wide, Label/Large 18/30--nu-type-label-large-*
Skip buttons40 × 40, radius 8, icon 28 (Figma 41.5 and 29.6, rounded)--nu-audio-player-skip-size
Play button36 circle, icon 20--nu-audio-player-play-size
Seek track8 tall (6px rail, 8px thumb). The invisible slider over it is 24 tall.--nu-audio-player-track-height, --nu-audio-player-hit-height
Title and timesLabel/Medium 16/28; times Subheading/2X Small 11/12, gap 3--nu-type-label-medium-*, --nu-type-subheading-2x-small-*
Close button52 circle, icon 20, 12 from the pill (40 in the narrow layout)--nu-audio-player-close-size, --nu-space-3
Episode cardPadding 16, gap 8, radius 20, 1px border; play pill 36 tall; ring 36; download 36--nu-episode-card-*

Values from the component CSS, which follows Figma's geometry (Control center 604:35881, episode cards 594:20678). When the player is narrower than 520px, title and slider move above the controls and the pill becomes a rounded card (code-only).

States and interactions

Episode states

Figma has three types. The status comes from progress unless you set status: nothing heard is new, part heard is in progress (time left and a ring), all heard is completed. Pass playing to the episode that is playing now.

  1. 1. Introduction to Design Principles

    The core principles behind products, systems and experiences, and how good design shapes everyday life.

  2. 2. Colour and contrast

    How colour carries meaning, and how to keep text readable for everyone.

  3. 3. Typography that teaches

    Choosing type for long reading, headings and small labels.

Audio playerPaused

Paused

Paused

Paused and playingThe play button shows Play or Pause, and its name changes with it.
Audio playerHover a control

Hover a control

Hover a control

Hover (code-only)Speed, skip and close buttons get Bg/Surface-800 on the dark pill and Bg/Soft-200 on the light pill. Figma has no hover for the player.
Audio playerPress Tab to focus

Press Tab to focus

Press Tab to focus

Focus (code-only)A 2px primary outline, 2px outside each control and 4px around the slider.
Audio playerLoading episode

Loading episode

Loading episode

Loading (code-only)Until the duration is known, the slider is disabled and the times read 0:00.

State tokens

Set on .nu-audio-player and .nu-episode-card. Override them on a wrapper only for a documented exception.

TokenDarkLight
--nu-audio-player-bgBg/Strong-950Bg/Soft-100
--nu-audio-player-fgText/WhiteText/Strong-950
--nu-audio-player-metaText/Soft-400Text/Sub-600
--nu-audio-player-hoverBg/Surface-800 (code-only)Bg/Soft-200 (code-only)
--nu-audio-player-play-bgBg/Soft-100Bg/Strong-950
--nu-audio-player-railState/Faded/Base (neutral/500)Bg/Soft-200
--nu-audio-player-fillBg/Soft-100neutral/700
--nu-audio-player-thumbneutral/700Bg/Surface-800 (neutral/800)
--nu-episode-card-bg / -bg-hoverBg/White / Bg/Weak-50
--nu-episode-card-ring-track / -ring-fillBg/Soft-200 / Bg/Strong-950 (stroke 4, code-only)

Composition examples

Podcast tab

Episode cards start an episode; the player appears below and keeps playing while people scroll. Pressing the playing episode's button pauses it. Each episode resumes where it stopped.

  1. 1. Introduction to Design Principles

    Why principles matter, and how they guide everyday product decisions.

    Download
  2. 2. Colour and contrast

    How colour carries meaning, and how to keep text readable for everyone.

    Download
  3. 3. Typography that teaches

    Choosing type for long reading, headings and small labels.

    Download

Choose an episode to open the player.

Localised labels

Every string is a labels entry. The time strings are functions, so you can say times naturally in each language.

Reproductor de audioIntroducción a los principios de diseño

Introducción a los principios de diseño

El diseño de las cosas cotidianas · Episodio 1

Introducción a los principios de diseño

El diseño de las cosas cotidianas · Episodio 1

API reference

AudioPlayer props

AudioPlayer props
PropTypeDefaultDescription
titlerequiredReactNode—Episode or track title. Also names the player group.
srcstring—Audio URL. Renders and drives an internal <audio>. Leave it out to drive the player with props.
audioRefRef<HTMLAudioElement>—Ref to the internal <audio> (only with src).
preload'none' | 'metadata' | 'auto''metadata'Native preload of the internal element.
playingboolean—Playing state (controlled). With src, the element is played or paused to match.
defaultPlayingbooleanfalseInitial playing state (uncontrolled, no src).
onPlayingChange(playing: boolean) => void—Called when playback starts or stops, from the controls or the element.
currentTimenumber—Position in seconds (controlled, no src).
defaultCurrentTimenumber0Initial position. With src, applied when the metadata loads.
onSeek(seconds: number) => void—Called when people seek with the slider or the skip buttons.
onTimeUpdate(seconds: number) => void—Called as the element's position updates (only with src). Use it to save progress.
durationnumber—Length in seconds. Required without src; with src it comes from the metadata.
playbackRatenumber—Playback speed (controlled).
defaultPlaybackRatenumber1Initial speed (uncontrolled).
onPlaybackRateChange(rate: number) => void—Called with the speed chosen from the menu.
playbackRatesreadonly number[][0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]Speeds in the speed menu (code-only list).
skipSecondsnumber15Seconds moved by the back and forward buttons. 5, 10, 15 and 30 have matching icons.
onClose() => void—Shows the close button (both layouts) and is called when it is pressed.
layout'dock' | 'pill''dock'The full-width, square bar for the bottom edge of an app (default), or the earlier floating pill, kept for existing uses.
subtitleReactNode—Dock: the show or episode line under the title.
artworkReactNode—Dock: the small cover, an image or a mark. Left out, there is no cover.
peaksreadonly number[]—Dock: real waveform data (0 to 1 over the whole episode). Left out, a shape that suggests speech is drawn.
expanded / defaultExpandedbooleanfalseDock: the expanded bar (controlled or initial). The compact bar is the default.
onExpandedChange(expanded: boolean) => void—Dock: called when the expand or minimize button is pressed.
onNext() => void—Dock: shows the next-episode button in the expanded bar.
appearance'dark' | 'light''dark'Pill surface: Bg/Strong-950 (Figma Light) or Bg/Soft-100 (Figma Dark).
labelsPartial<AudioPlayerLabels>AUDIO_PLAYER_LABELSOverrides for every string: player, play, pause, back(s), forward(s), speed, rate(r), seek, position(current, duration), close, clock(s).
menuContainerHTMLElement | null—Element for the speed menu portal (default document.body).
classNamestring—Class for the root .nu-audio-player.
refRef<HTMLDivElement>—Forwarded to the root.
…restOmit<HTMLAttributes<HTMLDivElement>, 'title' | 'children' | 'onPlay' | 'onPause' | 'onTimeUpdate'>—Any other attribute, spread on the root.

EpisodeCard props

EpisodeCard props
PropTypeDefaultDescription
titlerequiredReactNode—Episode title, rendered as a heading.
durationrequirednumber—Length in seconds, shown on the play button ("1h 29m").
numbernumber—Episode number before the title ("1.").
descriptionReactNode—Short summary under the title.
progressnumber0Seconds heard. Sets the status and the ring.
status'new' | 'in-progress' | 'completed'from progressFigma Type: Default, Played, Completed.
playingbooleanfalseThis episode is playing now; the button becomes Pause.
onPlayMouseEventHandler<HTMLButtonElement>—Called when the play button is pressed.
downloadHrefstring—Renders the download control as a link with download.
onDownloadMouseEventHandler<HTMLButtonElement>—Renders the download control as a button (when there is no downloadHref).
headingLevel2 | 3 | 4 | 5 | 63Heading level of the title.
labelsPartial<EpisodeCardLabels>EPISODE_CARD_LABELSplay, resume, replay, pause, download, duration(s), remaining(s).
refRef<HTMLElement>—Forwarded to the <article>.
…restOmit<HTMLAttributes<HTMLElement>, 'title'>—Any other attribute, spread on the article.

EpisodeList props

EpisodeList props
PropTypeDefaultDescription
childrenReactNode—EpisodeCards. Each is wrapped in an <li>.
aria-labelstring—Name of the list, e.g. the podcast title.
…restHTMLAttributes<HTMLOListElement>—Spread on the <ol>.

Helpers

formatClockTime (“1:02:23”), formatSpokenTime (“1 hour 2 minutes 23 seconds”), formatShortDuration (“1h 29m”), getEpisodeStatus, AUDIO_PLAYER_LABELS, AUDIO_PLAYER_RATES and EPISODE_CARD_LABELS are exported for building your own labels.

Styling hooks

The player renders .nu-audio-player (with data-appearance and data-playing), __bar, __speed, __skip, __play, __title, __seek (a Slider with .nu-slider__* parts), __times and __close. Cards render .nu-episode-card (with data-status) in .nu-episode-list. Use the --nu-audio-player-* and --nu-episode-card-* tokens to override.

Accessibility

Keyboard

KeyBehaviour
TabMoves through speed, back, play, forward, the slider and close, in that order.
Enter / SpaceActivates the focused button. On the speed button, opens the menu; arrows move, Enter chooses, Esc closes.
→ / ↑On the slider: forward 5 seconds.
← / ↓On the slider: back 5 seconds.
Page Up / Page DownOn the slider: forward or back 1 minute.
Home / EndOn the slider: start or end of the episode.

Screen readers

The player is a group named “Audio player” plus the title. Every control is a native button with a name (“Back 15 seconds”, “Play” or “Pause”, “Playback speed 1x”). The seek control is a native range input announced as “Seek, slider, 20 minutes 23 seconds of 1 hour 2 minutes 23 seconds”. The visible times are hidden, so they aren't read twice. Episode buttons say the action, the title and the time (“Resume 2. Colour and contrast 1h 2m left”); the ring is decorative.

Focus

A 2px primary outline with a 2px gap on every control, and around the whole track for the slider. Visible on both surfaces. The speed menu returns focus to its button when it closes.

Contrast and colour

Titles are 17.9:1 (white on Strong-950) and 16.4:1 (Strong-950 on Soft-100). Times are 7.2:1 (Soft-400 on Strong-950) and 6.1:1 (Sub-600 on Soft-100). On the dark pill the rail is 4.2:1 and the fill 3.9:1 against it. On the light pill the rail is State/Faded/Base, 3.88:1 on Soft-100, and the Strong-950 fill is 4.2:1 against it. Figma's Soft-200 rail was 1.1:1. Playing state is shown by the icon, not colour.

Touch and pointer

Buttons are 36 to 52px. The slider's input is 24px tall over the 8px track, so it is easy to grab and drag. Hover styles apply only on hover-capable pointers. Transitions stop with reduced motion.

You must: give every player a meaningful title; stop playback when the player closes; never autoplay without a user action; and pass translated labels in other languages, including the spoken position text.

Figma mapping

Figma propertyFigma valuesCode
Control center · Property 1Light · Darkappearance="dark" · appearance="light" (named by the pill colour)
Speed text“1x”playbackRate, speed menu (code-only list)
ProgressStatic barSeek slider (native range)
Close buttonAlways shownShown with onClose
Episode card · TypeDefault · Played · Completedstatus: new · in-progress · completed (or from progress)
Episode card · StateDefault · HoverCSS hover

Guidelines and related components

Use the audio player for

  • Podcast episodes and other long audio people listen to while they read.
  • A single, persistent player shared by a list of episodes.
  • Audio that has a transcript. Link it next to the player so deaf and hard-of-hearing learners get the same content (WCAG 1.2.1).

Don't use it for

  • Short sounds or pronunciation clips. Use a Button with a play icon.
  • Video. Use the browser's video controls or a video player.

Do and don't

Audio player2. Colour and contrast

2. Colour and contrast

2. Colour and contrast

DoShow one player at a time. A single player keeps one clear set of controls and one place to look.
Audio player1. Introduction

1. Introduction

1. Introduction

Audio player2. Colour and contrast

2. Colour and contrast

2. Colour and contrast

Don'tDon't put a player in every card. Many sliders and play buttons in a list are slow to navigate and easy to confuse.

2. Colour and contrast

DoNumber and name each episode. The title is part of every button's name, so the list makes sense out of context.

Episode

Don'tDon't use vague titles. “Play Episode, 1h 2m” doesn't say what people will hear.

Common mistakes

  • Starting playback on page load. Wait for the user to press play.
  • Keeping audio playing after the close button is pressed.
  • Translating the button names but not position, so the slider still speaks English times.
  • Passing both src and currentTime: with src, the position comes from the element.