Acrylic
Components

Audio Player

A floating capsule transport (Apple-Music mini-player) on the acrylic frosted surface.

A frosted capsule "now playing" bar. Two groups: an always-on transport (prev / play-pause / next, plus any right-side tools you pass) and a now-playing info group (cover + title + artist + a bottom seek rail) that appears only while a track is loaded. Fully controlled — wire the callbacks to your own audio engine. The optional volume control reveals a vertical Slider on hover.

Installation

npx shadcn add https://acrylic-ui.vercel.app/r/audio-player.json

Usage

The right-side tools are yours to pass via actions. In this example the queue button opens an "up next" Sheet from the right — a plain inline list, just to show the pattern; the player itself prescribes nothing about it.

Fantasma
Tainy, Jhayco
0:42/3:35
import { AudioPlayer } from "@/components/acrylic/audio-player"

<AudioPlayer
  track={{ title, artist, cover }}
  playing={playing}
  currentTime={time}
  duration={duration}
  volume={volume}
  hasPrev
  hasNext
  onToggle={toggle}
  onPrev={prev}
  onNext={next}
  onSeek={seek}
  onVolumeChange={setVolume}
/>

Pass track={null} (or omit it) for the idle state — the transport stays visible and the info group collapses. Drop the volume/onVolumeChange pair to hide the volume control.

Localization

Every user-facing string — the button aria-labels a screen reader announces, and the visible stand-in for an empty track.title — lives in one flat AudioPlayerLabels dictionary and defaults to English. Pass a partial labels to translate; it is merged over the defaults, so you override only the keys you translate:

<AudioPlayer
  labels={{ previous: "上一首", play: "播放", pause: "暂停", next: "下一首", volume: "音量" }}

/>
keydefaultwhere it shows
previous"Previous track"prev button aria-label
play / pause"Play" / "Pause"play-pause button aria-label (by state)
next"Next track"next button aria-label
volume"Volume"volume button + its slider aria-label
nowPlaying"Now playing"visible text when track.title is empty
openNowPlaying"Open now playing: {title}"mini only — the cover/title button's aria-label; {title} is substituted

DEFAULT_AUDIO_PLAYER_LABELS is exported if you want to read or spread the defaults. Nothing user-facing is hard-coded in the markup — a string you can't reach through labels is a bug, so report it rather than patching your vendored copy.

Mini variant

variant="mini" renders a compact chip for a sidebar footer or any tight rail — cover + title/artist + play/pause with the seek rail along the bottom, and no prev/next/volume/actions. It renders nothing when idle (so it takes no space); onOpen (cover/title click) is where you navigate to the full player. When the rail is too narrow for it (e.g. an icon-only sidebar), hide it — a cover-only stub isn't a usable player.

<AudioPlayer
  variant="mini"
  track={{ title, artist, cover }}
  playing={playing}
  currentTime={time}
  duration={duration}
  onToggle={toggle}
  onSeek={seek}
  onOpen={goToPlayer}
/>

For a persistent mini player, keep one global player (a single <audio> element) and provide its state above your whole layout — sidebar and content both — so the chip can read it from any page.

Full-screen stage

AudioPlayerStage is the immersive, full-screen face of the same player — an Apple-Music "now playing" view on a flowing Silk shader background. It shares AudioPlayer's controlled API (its track extends AudioPlayerTrack) and adds time-synced karaoke lyrics, a cover-derived background color that crossfades between tracks, and a real Fullscreen toggle. It installs separately, so the lightweight bar/mini transport never has to pull in the WebGL shader.

npx shadcn add https://acrylic-ui.vercel.app/r/audio-player-stage.json

The preview renders two triggers — Now Playing opens the stage windowed, Full screen opens it straight into the Fullscreen API (via autoFullscreen). Inside: ▾ or Escape to close, ⤢ to toggle fullscreen, prev/next to switch tracks and watch the background recolor.

import { AudioPlayerStage } from "@/components/acrylic/audio-player-stage"

<AudioPlayerStage
  track={{
    title,
    artist,
    cover,
    lyrics: [ { time: 0, text: "…" }, { time: 12, text: "…" } ], // omit → no-lyrics layout
  }}
  playing={playing}
  currentTime={time}       // seconds; the stage runs its own smooth clock for karaoke
  duration={duration}
  volume={volume}
  extractFromCover         // pull the background color from the cover art…
  // accentColor="#5E3AA8" // …or set it explicitly (skips extraction)
  colorTransitionMs={400}
  onToggle={toggle}
  onPrev={prev}
  onNext={next}
  onSeek={seek}
  onVolumeChange={setVolume}
  onClose={close}
/>
  • Lyrics are LRC-style { time, text } lines; the active line fills bright, karaoke-style, as playback crosses it. Omit lyrics (or pass []) and the lyrics pane disappears — the player centers instead.
  • Background color comes from the cover when extractFromCover is set (needs a same-origin or CORS-enabled cover; otherwise it falls back to accentColor, then a default) and crossfades over colorTransitionMs.
  • Fullscreen: the ⤢ button calls the Fullscreen API on the stage (browsers require a real user click).

Stage-only props

proptypedefaultdescription
trackAudioPlayerStageTrackAudioPlayerTrack + optional lyrics: { time, text }[]
accentColorstringforce the background color (skips cover extraction)
extractFromCoverbooleantruepull the background color from the cover art
colorTransitionMsnumber400crossfade duration between track colors (0 = snap)
nowPlayingLabelstringoptional eyebrow caption above the cover (e.g. "FROM YOUR LIBRARY"); off when unset
labelsPartial<AudioPlayerStageLabels>Englishoverride the stage's own UI copy — see below
onClose() => voidthe collapse (▾) button / Escape

The stage has its own label dictionary, AudioPlayerStageLabels (defaults exported as DEFAULT_AUDIO_PLAYER_STAGE_LABELS), merged the same way as AudioPlayer's: collapse, enterFullscreen / exitFullscreen, previous, play / pause, next, progress, volume, and unknownTrack (the visible stand-in for an empty track.title).

labels and nowPlayingLabel are not interchangeable, and deliberately can't set the same string: labels is the component's own UI copy — fixed strings that vary only by locale — while nowPlayingLabel is caller content that varies per instance. That's why a missing title falls back to labels.unknownTrack and never to nowPlayingLabel: an eyebrow caption rendered in the title slot reads as if it were the track's name.

The playback props (playing, currentTime, duration, volume, hasPrev/hasNext, onToggle/onPrev/onNext/onSeek/onVolumeChange) match AudioPlayer above.

Props

proptypedefaultdescription
variant"bar" | "mini""bar"full transport bar, or a compact sidebar/rail chip
onOpen() => voidmini only — cover/title clicked (open the full player)
trackAudioPlayerTrack | null{ title, artist?, cover? }; null/omitted = idle (bar: info hidden; mini: renders nothing)
playingbooleanfalseplay/pause icon state
currentTimenumber0elapsed seconds (time readout + seek fill)
durationnumber0total seconds
volumenumber0..1; pass with onVolumeChange to show the hover volume slider
hasPrev / hasNextbooleanfalseenable the prev / next buttons
onToggle() => voidplay/pause clicked
onPrev / onNext() => voidskip clicked
onSeek(seconds: number) => voidseek rail clicked
onVolumeChange(volume: number) => voidvolume slider dragged (0..1)
actionsReactNodeextra tool buttons at the right end of the transport
labelsPartial<AudioPlayerLabels>Englishoverride the built-in strings — see Localization

Plus all native <div> props.

On this page