Scroll is not reset on navigation when a parallel route slot renders only a position: fixed element (appNewScrollHandler regression in 16.3.0)
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 142k
- Forks
- 32.4k
- Avg merge
- 2d 14h
- Merged PRs (30d)
- 351
Description
Link to the code that reproduces this issue
https://github.com/Pilaton/next-fixed-slot-scroll-repro
To Reproduce
- Create an app with a parallel route slot whose only rendered element is
position: fixed, e.g. a@headerslot:
app/
layout.tsx // renders {header} {children}
@header/page.tsx // <header className="fixed top-2"> ... </header>
@header/[...rest]/page.tsx
about/page.tsx
pnpm dev, open/, scroll down ~2000px.- Click a
<Link>to/about.
Current vs. Expected behavior
Current (16.3.0): the new page opens at the previous scroll offset. If the target page is shorter than the scroll offset, it opens with the footer on screen.
Expected (16.2.11 and earlier): the new page opens at the top.
Cause
experimental.appNewScrollHandler changed default from false to true:
16.2.11—dist/server/config-shared.js:appNewScrollHandler: false16.3.0—dist/server/config-shared.js:appNewScrollHandler: true
InnerScrollAndFocusHandlerOld skipped fixed/sticky elements when picking the scroll target, with an explicit comment stating why:
function shouldSkipElement(element) {
// we ignore fixed or sticky positioned elements since they'll likely pass the "in-viewport" check
// and will result in a situation we bail on scroll because of something like a fixed nav,
// even though the actual page content is offscreen
if (['sticky', 'fixed'].includes(getComputedStyle(element).position)) {
return true;
}
...
}
InnerScrollHandlerNew (the Fragment-ref fork that is now the default) has no equivalent check. It passes the Fragment ref straight to getScrollTargetState, which reads the top edge of whatever host children the slot rendered:
return elementTop >= getScrollPaddingTop() && elementTop <= viewportHeight ? 1 : 2;
For a slot containing only a fixed header, elementTop is a small positive number on every navigation, so the result is always 1 ("already in viewport"). The handler then marks the shared scrollRef as handled and returns without scrolling:
didHandleScroll = true;
// Mark as scrolled so no other segment scrolls for this navigation.
scrollRef.current = false;
...
if (initialTargetState === 1) {
return;
}
Because accumulateScrollRef assigns the same scrollRef object to every changed cache node, the slot that renders the fixed header claims the scroll intent before the children slot's layout effect runs, and the actual page content is never scrolled into view.
Verification
On a 430x932 viewport, 7 navigations from a scroll offset of 2000:
| condition | navigations that reset scroll to 0 |
|---|---|
default (appNewScrollHandler: true) |
0 / 7 |
html { scroll-padding-top: 200px } |
7 / 7 |
experimental.appNewScrollHandler: false |
7 / 7 |
The scroll-padding-top row changes no layout at all — it only raises the threshold inside getScrollTargetState. That it alone flips the outcome isolates the behavior to that comparison.
Workaround
// next.config.ts
experimental: {
appNewScrollHandler: false,
},
Provide environment information
Operating System:
Platform: darwin
Arch: x64
Version: Darwin Kernel Version 24.6.0
Available memory (MB): 32768
Available CPU cores: 6
Binaries:
Node: 26.7.0
npm: 11.19.0
pnpm: 11.20.0
Relevant Packages:
next: 16.3.0 // Latest available version is detected (16.3.0).
react: 19.2.8
react-dom: 19.2.8
typescript: 6.0.3
Next.js Config:
output: N/A
Which area(s) are affected? (Select all that apply)
Linking and Navigating, Parallel & Intercepting Routes
Which stage(s) are affected? (Select all that apply)
next dev (local), next start (local), Vercel (Deployed)
Additional context
No response
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the appNewScrollHandler configuration and compare InnerScrollHandlerOld with InnerScrollHandlerNew, especially getScrollTargetState and the shared scrollRef behavior. Reproduce the issue with the linked next-fixed-slot-scroll-repro app, then verify that navigation from a scrolled page resets the content scroll position when a parallel slot contains only a fixed element.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, nextjs, react
- Domain
- frontend, web-dev
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100