WebberUI

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.

Loading preview…
npx shadcn@latest add https://webberui.com/r/now-playing-sheet.json

Playground

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.json

Or, 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

FieldTypeDescription
titlestringTrack title
artiststringPerformer
albumstringAlbum / context title (shown at the top of the expanded page)
artworkReact.ReactNodeArtwork content (an <img>, a gradient <div>, or any node); when omitted, a neutral gradient placeholder is shown
durationnumberTotal track length (seconds), used for the time labels and automatic advancing

Props

PropTypeDefaultDescription
trackNowPlayingTrackData for the currently playing track
childrenReact.ReactNodeApp content above the mini bar (a scrollable area)
openbooleanControlled expanded state
defaultOpenbooleanfalseUncontrolled initial expanded state
onOpenChange(open: boolean) => voidExpanded-state change callback
playingbooleanControlled playing state
defaultPlayingbooleanfalseUncontrolled initial playing state
onPlayingChange(playing: boolean) => voidPlaying-state change callback
progressnumberControlled playback progress (0–1); providing it disables automatic advancing
onProgressChange(progress: number) => voidProgress change callback
onPrevious() => voidTapping "previous"
onNext() => voidTapping "next"
classNamestringAppended 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 another layoutId and 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.duration and 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 a role="dialog" labelled by the track title via aria-labelledby.
  • While expanded, Escape collapses it and focus moves into the now-playing page; focus returns to the mini bar after collapsing. While expanded, the mini bar gets inert to leave the focus order.
  • The progress bar is a role="slider" with aria-valuenow and aria-valuetext; the play/pause and shuffle/repeat buttons all have aria-label and aria-pressed.
  • With "reduce motion" enabled, the morph offsets and dragging are disabled in favor of a brief fade in / fade out.

On this page