Skip to content

fix: add opt-in scroll restoration for view transitions#66

Open
sauravhathi wants to merge 1 commit into
shuding:mainfrom
sauravhathi:fix/scroll-restoration
Open

fix: add opt-in scroll restoration for view transitions#66
sauravhathi wants to merge 1 commit into
shuding:mainfrom
sauravhathi:fix/scroll-restoration

Conversation

@sauravhathi

Copy link
Copy Markdown

Problem

When <ViewTransitions> wraps a Next.js app, scroll restoration stops working. Navigating to a page and pressing back always scrolls to the top.

Root cause: The popstate handler in browser-native-events.ts wraps navigation in document.startViewTransition(), which defers DOM updates and prevents the browser's/Next.js's scroll restoration from working.

Solution

Adds opt-in scroll restoration via the options prop:

<ViewTransitions options={{ scroll: true }}>
  {children}
</ViewTransitions>

When enabled:

  1. Scroll positions are persisted into history.state via replaceState (debounced at 100ms)
  2. On popstate, the saved position is read from the target history entry
  3. Scroll is restored before the view transition resolves, so the new DOM snapshot captures the correct position

This follows the same pattern used by Remix and React Router — positions are stored in history.state.

Changes

  • browser-native-events.ts: Added saveScrollPosition helper, debounced scroll listener, and scroll restore before transition resolve
  • transition-context.tsx: Added ViewTransitionsOptions type and options prop to ViewTransitions component

Breaking Changes

None. Scroll restoration is opt-in and disabled by default.

Fixes #58
Fixes #12

When <ViewTransitions> wraps a Next.js app, the popstate handler's use of
document.startViewTransition() defers DOM updates, preventing the browser's
native scroll restoration from working. Back/forward navigation always
scrolls to the top.

This adds an opt-in scroll restoration mechanism via the options prop:

  <ViewTransitions options={{ scroll: true }}>

When enabled, scroll positions are persisted into history.state (via
replaceState) on a debounced scroll listener and restored before the view
transition resolves on popstate navigation. This follows the same pattern
used by Remix and React Router.

Changes:
- browser-native-events.ts: Added saveScrollPosition helper, debounced
  scroll listener, and scroll restore before transition resolve
- transition-context.tsx: Added ViewTransitionsOptions type and options
  prop to ViewTransitions component

Fixes shuding#58
Fixes shuding#12
@vercel

vercel Bot commented Mar 26, 2026

Copy link
Copy Markdown

@sauravhathi is attempting to deploy a commit to the shu Team on Vercel.

A member of the Team first needs to authorize it.

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.

Snapshot is only taken on router.back() snapshot is only taken after scroll restoration Scroll restoration doesn't work

1 participant