Now Playing Sheet
A persistent mini player bar at the bottom; tap it and the artwork scales up as a shared element while the whole layer springs open into a full-screen now-playing page, dismissed by a downward drag whose gesture velocity decides the outcome.
npx shadcn@latest add https://webberui.com/r/now-playing-sheet.jsonPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<NowPlayingSheet />
Installation
npx shadcn@latest add https://webberui.com/r/now-playing-sheet.jsonOr, once registries are configured in components.json, install it as @webberui/now-playing-sheet.
Usage
NowPlayingSheet is itself a positioning container: pass your app content in as children (it gets a strip of padding covered by the mini bar pinned to the bottom), and the mini bar and the full-screen now-playing page share the same track.
import { NowPlayingSheet } from "@/components/ui/now-playing-sheet";
<div className="relative h-[560px] w-full max-w-sm overflow-hidden rounded-3xl">
<NowPlayingSheet
className="size-full"
track={{
title: "Neon Drift",
artist: "Night Current",
album: "Midnight Loop",
artwork: <img src="/cover.jpg" alt="" />,
duration: 214,
}}
defaultPlaying
onNext={() => {}}
onPrevious={() => {}}
>
{/* your app content */}
</NowPlayingSheet>
</div>;Expansion, playback, and progress all support controlled and uncontrolled modes: pass open, playing, or progress to take control, otherwise the component manages them internally. When uncontrolled and playing, progress advances automatically based on track.duration.
NowPlayingTrack
| Field | Type | Description |
|---|---|---|
title | string | Track title |
artist | string | Performer |
album | string | Album / context title (shown at the top of the expanded page) |
artwork | React.ReactNode | Artwork content (an <img>, a gradient <div>, or any node); when omitted, a neutral gradient placeholder is shown |
duration | number | Total track length (seconds), used for the time labels and automatic advancing |
Props
| Prop | Type | Default | Description |
|---|---|---|---|
track | NowPlayingTrack | — | Data for the currently playing track |
children | React.ReactNode | — | App content above the mini bar (a scrollable area) |
open | boolean | — | Controlled expanded state |
defaultOpen | boolean | false | Uncontrolled initial expanded state |
onOpenChange | (open: boolean) => void | — | Expanded-state change callback |
playing | boolean | — | Controlled playing state |
defaultPlaying | boolean | false | Uncontrolled initial playing state |
onPlayingChange | (playing: boolean) => void | — | Playing-state change callback |
progress | number | — | Controlled playback progress (0–1); providing it disables automatic advancing |
onProgressChange | (progress: number) => void | — | Progress change callback |
onPrevious | () => void | — | Tapping "previous" |
onNext | () => void | — | Tapping "next" |
className | string | — | Appended to the outermost stage container (set size / radius / background) |
How it works
- Shared element scale-up: the artwork in the mini bar and on the now-playing page share one
layoutId, so Motion morphs them seamlessly when expanding and collapsing; the whole player bar shares anotherlayoutIdand springs from the bottom rectangle into the full-screen layer. - Gesture dismissal: the full-screen layer can be dragged downward; on release, an offset over 120px or a velocity over 600px/s collapses it, otherwise it springs back into place.
- Progress bar: can be clicked, dragged, or adjusted with arrow keys /
Home/End(5 seconds or 2% per step). - Automatic advancing: when uncontrolled and playing, progress advances every 250ms based on
track.durationand stops at the end; the timer is cleared on unmount and on state changes.
Accessibility
- The artwork area of the mini bar is an expand button with
aria-expanded, and the full-screen layer is arole="dialog"labelled by the track title viaaria-labelledby. - While expanded,
Escapecollapses it and focus moves into the now-playing page; focus returns to the mini bar after collapsing. While expanded, the mini bar getsinertto leave the focus order. - The progress bar is a
role="slider"witharia-valuenowandaria-valuetext; the play/pause and shuffle/repeat buttons all havearia-labelandaria-pressed. - With "reduce motion" enabled, the morph offsets and dragging are disabled in favor of a brief fade in / fade out.
Control Center Panel
A grid of glass control modules; long-press any tile and it springs open in place into a fine-grained control card (slider, segmented toggle) while the background blurs and recedes, springing back on release.
Large Title Collapse Header
An iOS-style large-title header that shrinks into a centered small title as a shared element on scroll, pulls out a search bar with a rubber-band bounce, and sticks section headings one after another below the header.