App Shells & Panels

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.

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add now-playing-sheet.en

New project or installation trouble? Start here →

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

Active edited usageDefault props
<NowPlayingSheet/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

Installation

npx webberui@latest add https://webberui.com/r/now-playing-sheet.en.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
trackNowPlayingTrack—Data for the currently playing track
childrenReact.ReactNode—App content above the mini bar (a scrollable area)
openboolean—Controlled expanded state
defaultOpenbooleanfalseUncontrolled initial expanded state
onOpenChange(open: boolean) => void—Expanded-state change callback
playingboolean—Controlled playing state
defaultPlayingbooleanfalseUncontrolled initial playing state
onPlayingChange(playing: boolean) => void—Playing-state change callback
progressnumber—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"
classNamestring—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 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.