Skip to content

feat(motion): View Transitions — Now Playing sheet, art morphs, smooth skip (#289) - #320

Merged
lolimmlost merged 7 commits into
mainfrom
feat/view-transitions-sheet
Oct 10, 2026
Merged

lolimmlost merged 7 commits into
mainfrom
feat/view-transitions-sheet

Conversation

@lolimmlost

Copy link
Copy Markdown
Owner

Closes #289.

Adds animated page and player transitions using the browser's View Transitions API. Where the API isn't supported, or Reduce Motion is on, everything updates instantly, as it did before. These are visual changes only; nothing touches the audio path.

What changes

  • Opening Now Playing: the mini-player art morphs into the big art. The fullscreen view slides up as a sheet while the page behind it recedes. Closing reverses this.
  • Album covers: tapping a cover in a list morphs it into the album page's cover. Other page changes get a soft cross-fade.
  • Swipe to skip: one continuous motion. The current art slides off, and the next art slides in from the other side. It no longer jumps when the art settles.
  • No pop-in: the big art is preloaded and decoded before the morph runs, and so are the previous/next songs' art. Layering is fixed so the art stays above the sheet.
  • Song title in fullscreen: tapping it closes the sheet, opens the album page, and morphs the big art into that page's cover. The artist link still just closes the sheet.

Implementation

  • src/lib/utils/view-transition.ts
    • withViewTransition, plus withViewTransitionAsync for async updates such as a route change.
    • waitForImage, so a morph lands on a loaded image instead of an empty placeholder.
    • Typed transitions (np-open, np-close, np-to-album), with feature detection.
  • src/styles.css: keyframes for the sheet and page recede, z-index on the transition groups, and timing.
  • PlayerBar.tsx, NowPlayingFullscreen/*, ArtMode.tsx (skip animation via the Web Animations API), album page [data-album-hero-art].

Testing

  • Unit tests in view-transition.test.ts (9 tests). The full suite passes: 852 tests.
  • npm run build is OK.
  • Tested on a prod deploy of this branch as an iOS PWA: open/close, skip, and title → album.

Follow-up

🤖 Generated with Claude Code

https://claude.ai/code/session_01LAQj7kfXYU9jy1L9ByWw8r

claude and others added 7 commits October 7, 2026 02:17
…overs into album pages, soft page transitions (#289)

- Page navigations: only the main content (view-transition-name: app-main)
  fades/lifts; sidebar, player and tab bar stay put. Router uses typed
  transitions so search-param-only updates (e.g. typing in search) don't
  animate; engines without typed transitions just navigate instantly.
- Now Playing: tapping the mini-player art starts a transition, flushes the
  open synchronously, and the art morphs into the fullscreen artwork; close
  (button or swipe-down from the drag position) morphs back. Fullscreen gets
  skipSlide for these opens; lyrics/visualizer opens keep the slide-up. Mode
  reset moved into render so the first transition frame is already correct.
- Albums: the tapped card's cover (named at tap time, so duplicates across
  lists can't clash) morphs into the album header; the album page shows the
  cover while details load so there's always a target.
- view-transition.ts helpers (feature-detected, reduced-motion aware);
  reduced motion disables every transition animation. No new dependencies,
  nothing on the audio path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SjSuKavpRLaVq5buHPm9ce
…round the art morph (#289)

On iOS PWA the open looked like the art sliding and then the background
snapping in: only the art had a view-transition-name, so the fullscreen
backdrop/controls were part of the root snapshot and just cross-faded.

- Name the fullscreen container `np-sheet` when opened via a view transition.
- Sheet slides up from the bottom on open / down on close (`:only-child`, so it
  works even without typed-transition support), on the art's curve.
- On np-open / np-close the page behind recedes (scale 0.94, dim) / returns
  instead of cross-fading two identical snapshots.
- Art morph duration 380 → 440 ms to match the sheet.
- Reduced motion: unchanged (all view-transition animations off).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LAQj7kfXYU9jy1L9ByWw8r
…eload the big art (#289)

Two causes of "album pops in after the background":
- No explicit stacking between transition groups, so Safari could paint the art
  group under the sliding np-sheet. Now: art/album-art z-index 3 > sheet 2 > page.
- The fullscreen view renders a 600px cover; the mini player shows a small one.
  If the 600px image wasn't decoded when the transition captured its "after"
  state, the morph went to an empty frame and the image appeared later.
  PlayerBar now preloads the 600px cover (same URL as ArtMode) whenever the
  song changes, and the open waits up to 200 ms for img.decode() before
  starting the transition.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LAQj7kfXYU9jy1L9ByWw8r
… art settles (#289)

Swipe-skip in the fullscreen art ran two animations at once: an inline
snap-back transition from the drag position AND a CSS slide-in class added a
render later (after the track change), removed by a separate 300 ms timer. Their
different start times/positions made the art jump as they settled, and the new
song's 600px art often wasn't loaded yet.

- ArtMode: on a committed swipe, the current art continues in the swipe
  direction and fades (140 ms), the track changes, then the new art slides in
  from the opposite side (280 ms), all via the Web Animations API from the
  release position; inline styles are cleared at the hand-off. Non-skip
  releases still spring back. Reduced motion: instant skip. Removed the
  swipeDirection state/class/timer.
- PlayerBar: also preload the previous/next songs' 600px art so the incoming
  image is already decoded when it lands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LAQj7kfXYU9jy1L9ByWw8r
…e cover (#289)

Tapping the song/album link in fullscreen Now Playing used to just close the
sheet back to the mini player. It now runs one view transition that closes the
sheet, navigates to the album, waits (up to 600ms) for the cover image, and
morphs the big art into the album hero art.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LAQj7kfXYU9jy1L9ByWw8r
- Main content now recedes with the rest of the page on open/close
  (app-main is its own layer, so it needs the np-open/np-close rules too)
- An album page's cover no longer floats above the sliding sheet: its
  album-art name is suppressed for np-open/np-close and restored after
- Title -> album page unmounts the sheet in the same commit, so the morph
  works even when the sheet was opened from lyrics/visualizer
- Artist link closes the sheet and navigates as one transition instead of
  the router's transition aborting the slide-down
- Cmd/ctrl/shift-click on the title/artist links opens a new tab again

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LAQj7kfXYU9jy1L9ByWw8r
… slides down (#289)

The sheet's blurred art tint is scaled to 110% and blurred, so it bleeds
past the sheet's edges. The np-sheet snapshot included that bleed. After
the slide-down a strip of it sat at the bottom of the screen until the
transition ended, then vanished with no fade. Clip the sheet to its bounds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LAQj7kfXYU9jy1L9ByWw8r
@lolimmlost
lolimmlost merged commit 21a4332 into main Oct 10, 2026
3 checks passed
lolimmlost added a commit that referenced this pull request Oct 10, 2026
…op (#289)

view-transition-name on <main> (app-main) makes it a stacking context all
the time, not only during transitions. The album and search pages' fixed,
negative-z backdrop then paints as part of <main>'s layer, above the
non-positioned, translucent left sidebar, so the sidebar sat under the
backdrop's gradient. Put both sidebars on their own layer above <main>,
which restores the pre-#320 stacking.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LAQj7kfXYU9jy1L9ByWw8r
lolimmlost added a commit that referenced this pull request Oct 10, 2026
…ing transitions (#289) (#324)

* fix(motion): album page's blurred backdrop no longer covers the sidebars during transitions (#289)

The album and search pages draw a full-window blurred backdrop (position:
fixed, behind the content). <main> is its own view-transition layer
(app-main), and its snapshot includes fixed descendants, so for the whole
transition that backdrop painted over the sidebars on desktop. Most visible
going from Now Playing's song title to the album page. Clip the app-main
group to <main>'s box.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LAQj7kfXYU9jy1L9ByWw8r

* fix(motion): sidebars no longer dimmed by the album backdrop on desktop (#289)

view-transition-name on <main> (app-main) makes it a stacking context all
the time, not only during transitions. The album and search pages' fixed,
negative-z backdrop then paints as part of <main>'s layer, above the
non-positioned, translucent left sidebar, so the sidebar sat under the
backdrop's gradient. Put both sidebars on their own layer above <main>,
which restores the pre-#320 stacking.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LAQj7kfXYU9jy1L9ByWw8r

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Motion: View Transitions — album-art morph into Now Playing + route transitions

2 participants