Skip to content

shadcn.io is an independent project built on shadcn/ui

Components

Free

Audio Player for Radix UI

Generated speech or any audio file, under a row of controls of your own. For React with Radix UI: play, seek, time, mute and volume.

A voice reply plays from one row of controls: play, seek back and forward, the time, a seek bar, the length, mute and volume.

Use it for text-to-speech output, voice messages and audio previews, where the browser's own player looks out of place in a chat. For a video, use Video Player. In React, the controls are Buttons and range inputs wired to a native audio element's events, so the browser still does the decoding and the buffering, and there's no media library underneath.

Preview width

Installation

Sign in to get your install command
Free with an account. It carries your token, so the CLI installs everything in one go.

Anatomy

Audio Player anatomy: control bar, play button, seek buttons, time range, duration display, volume range
AudioPlayer
├── AudioPlayerElement (audio)
└── AudioPlayerControlBar
    ├── AudioPlayerPlayButton
    ├── AudioPlayerSeekBackwardButton
    ├── AudioPlayerSeekForwardButton
    ├── AudioPlayerTimeDisplay
    ├── AudioPlayerTimeRange
    ├── AudioPlayerDurationDisplay
    ├── AudioPlayerMuteButton
    └── AudioPlayerVolumeRange

Usage

import {
  AudioPlayer, AudioPlayerControlBar, AudioPlayerDurationDisplay,
  AudioPlayerElement, AudioPlayerMuteButton, AudioPlayerPlayButton,
  AudioPlayerTimeDisplay, AudioPlayerTimeRange, AudioPlayerVolumeRange,
} from "@/components/ui/audio-player";
<AudioPlayer>
  <AudioPlayerElement src="/reply.mp3" />
  <AudioPlayerControlBar>
    <AudioPlayerPlayButton />
    <AudioPlayerTimeDisplay />
    <AudioPlayerTimeRange />
    <AudioPlayerDurationDisplay />
    <AudioPlayerMuteButton />
    <AudioPlayerVolumeRange />
  </AudioPlayerControlBar>
</AudioPlayer>

Accessibility

Every button has a name that says what it does now (Play or Pause, Mute or Unmute, Back 10 seconds), and the two ranges are native range inputs named Seek and Volume, the seek bar reading its time as "0:01 of 0:02". The control bar is a group named Audio controls. It stays left to right in right-to-left text, as media controls do: they follow the tape, not the reading direction. The shortcuts work while focus is anywhere inside the player; a focused button keeps Space and Enter, and a focused range keeps the arrows.

Space
Play or pause (anywhere but a button or a range).
K
Play or pause.
Arrow Left
Back 5 seconds.
Arrow Right
Forward 5 seconds.
J
Back 10 seconds.
L
Forward 10 seconds.
Arrow Up
Volume up.
Arrow Down
Volume down.
M
Mute or unmute.
Home
Go to the start.
End
Go to the end.

Demos

Compact

A voice message: a filled play button, the seek bar and the length, in a pill. Leave out the parts you don't need; each one reads the player from context, so any of them can go anywhere inside AudioPlayer.

Preview width

Seek

Seek buttons on both sides of play, 2 seconds each way through seekOffset (10 by default), and the time counting down with remaining. This clip loads nothing until it's played (preload="none"), so its length shows as --:-- until then.

Preview width

Transcript

The player and a Transcription share one time. onCurrentTimeChange reports the position as the audio plays, and a new currentTime seeks it, so a click on a sentence moves the player.

Preview width

AI SDK

generateSpeech returns the audio as result.audio, with its bytes as base64 and its type as mediaType. Send those two to the page from your server route or action.

import { openai } from "@ai-sdk/openai";
import { generateSpeech } from "ai";

export async function speak(text: string) {
  const { audio } = await generateSpeech({
    model: openai.speech("tts-1"),
    text,
    voice: "alloy",
  });

  return { base64: audio.base64, mediaType: audio.mediaType };
}

Pass them to AudioPlayerElement as data, and it plays them as a data: URL.

<AudioPlayerElement data={speech} />

A data URL keeps the whole file in the page, which suits a reply of a few seconds. For a long reading, store the file and pass its URL as src.

Migrating from AI SDK Elements

If you added Audio Player from AI SDK Elements (opens in a new tab), your project has a components/ai-elements/audio-player.tsx built on media-chrome. Install this one, import from @/components/ui/audio-player instead, then delete the old file and remove media-chrome from your dependencies if nothing else uses it. Every part keeps its name, and src, data, seekOffset, remaining and showDuration work as before. What differs:

  • AudioPlayer is a div, not a MediaController: media-chrome's attributes and its --media-* CSS variables do nothing here. Style the parts with className.
  • AudioPlayerElement needs no slot="media", and it loads the audio's length on its own (preload="metadata").
  • AudioPlayer adds currentTime and onCurrentTimeChange, to follow and move the audio from outside.

FAQ

How do I play generated speech in Audio Player?

Pass the audio from the AI SDK's generateSpeech to AudioPlayerElement as data, an object with base64 and mediaType; it builds the data: URL. Any other file goes in src, as a URL. AudioPlayerElement takes every attribute an audio element takes, preload, loop and muted included.

Which controls does Audio Player have?

Play and pause, seek backward and forward (10 seconds unless seekOffset says otherwise), the time, a seek bar, the length, mute and volume. Use them all or only the ones you need, in any order inside AudioPlayerControlBar, as the Compact demo does with three of them.

Which keyboard shortcuts does Audio Player have in React?

With focus anywhere inside it: Space and K play and pause, the left and right arrows seek 5 seconds, J and L seek 10, the up and down arrows change the volume, M mutes, Home and End jump to the ends. A focused button keeps Space and Enter, and a focused range keeps its arrows.

How do I keep a transcript in step with Audio Player?

Keep the time in your own state: onCurrentTimeChange reports it as the audio plays, and setting currentTime seeks the audio. Pass the same state to Transcription as its currentTime, and its onSeek to the same setter, as the Transcript demo does.

How does Audio Player match my theme?

It's drawn with your theme's tokens: outline and ghost Buttons, the ranges in primary on input, the times in muted-foreground. Dark mode follows. AudioPlayer itself has no border or background, so it sits inside a message or a card; give it className for a frame of its own, as the demos do.

Why don't the controls mirror in right-to-left text?

Media controls follow the tape, not the reading direction: the seek bar fills left to right and back points left in any language, as on most players. AudioPlayerControlBar sets dir="ltr" for that reason. Pass dir="rtl" to it if you want the bar mirrored anyway.

How do I install Audio Player in React?

Run the command in the Installation section on this page. It adds the player's parts to your ui folder, with the Button they're built on and lucide for the icons. It has no media library underneath: the audio element does the playing. The command uses the shadcn CLI.

API Reference

AudioPlayer

PropType
currentTime?number

The playback position in seconds. When it changes, the audio seeks to it, so a transcript or a list of chapters can move the player.

onCurrentTimeChange?(seconds: number) → void

Called with the position in seconds as the audio plays and seeks.

Parameters

seconds – number

Returns
void
className?string

Merged with the part's classes.

AudioPlayerElement

<audio> data-slot="audio-player-element"

PropType
data?object

Generated speech to play, turned into a data: URL; or give src.

Type
{ base64: string; mediaType: string }
loop?boolean

Plays it again from the start when it ends.

muted?boolean

Starts it without sound.

preload?"" | "none" | "metadata" | "auto"

How much to load before it plays: none waits for a press, metadata loads its length, auto may load it all.

Type
"" | "none" | "metadata" | "auto"
Default
"metadata"
src?string

The audio's URL; or give data.

className?string

Forwarded to the part.

AudioPlayerControlBar

<div> data-slot="audio-player-control-bar"

PropType
className?string

Merged with the part's classes.

AudioPlayerPlayButton

data-slot="audio-player-play-button"

PropType
clickEffect?boolean

Apply a click effect to the button.

Default
true
isLoading?boolean

Show a loading indicator.

Default
false
asChild?boolean

Merge props onto the child element instead of rendering a host.

className?string

Merged with the part's classes.

AudioPlayerSeekBackwardButton

data-slot="audio-player-seek-backward-button"

PropType
clickEffect?boolean

Apply a click effect to the button.

Default
true
isLoading?boolean

Show a loading indicator.

Default
false
seekOffset?number

How far a press seeks, in seconds.

Default
10
asChild?boolean

Merge props onto the child element instead of rendering a host.

className?string

Merged with the part's classes.

AudioPlayerSeekForwardButton

data-slot="audio-player-seek-forward-button"

PropType
clickEffect?boolean

Apply a click effect to the button.

Default
true
isLoading?boolean

Show a loading indicator.

Default
false
seekOffset?number

How far a press seeks, in seconds.

Default
10
asChild?boolean

Merge props onto the child element instead of rendering a host.

className?string

Merged with the part's classes.

AudioPlayerTimeDisplay

<span> data-slot="audio-player-time-display"

PropType
remaining?boolean

Counts down the time left instead of up.

Default
false
showDuration?boolean

Shows the duration after the time, 0:42 / 3:12.

Default
false
className?string

Merged with the part's classes.

AudioPlayerDurationDisplay

<span> data-slot="audio-player-duration-display"

PropType
className?string

Merged with the part's classes.

CSS variableType
--audio-player-range-fillstring

AudioPlayerTimeRange

<div> data-slot="audio-player-time-range"

PropType
className?string

Merged with the part's classes.

AudioPlayerMuteButton

data-slot="audio-player-mute-button"

PropType
clickEffect?boolean

Apply a click effect to the button.

Default
true
isLoading?boolean

Show a loading indicator.

Default
false
asChild?boolean

Merge props onto the child element instead of rendering a host.

className?string

Merged with the part's classes.

AudioPlayerVolumeRange

<div> data-slot="audio-player-volume-range"

PropType
className?string

Merged with the part's classes.