How to Fix: Next Link component scroll={false} not working as expected
Next.js Link with scroll={false} can look broken even when the prop is being passed correctly. The confusing part is that this flag only disables Next.js automatic scroll reset during a client-side navigation. It does not guarantee that the browser, the new route, nested layouts, hash changes, focus restoration, or manually triggered scrolling logic will keep the viewport in place.
Table of Contents
Understanding the Root Cause
In the App Router, Next.js tries to manage scroll position during navigation so route changes feel natural. When you use <Link scroll={false} />, you are only telling Next.js: do not automatically scroll to the top after this client-side navigation.
That sounds simple, but several things make it appear as if the prop is ignored:
- A different scroll container is involved. If your page scrolls inside a wrapper with
overflow: autoinstead of the window, preserving window scroll will not preserve the container scroll. - The destination route is remounted. If a layout, page, or large subtree is recreated, the DOM may reset and visually jump even without an explicit scroll-to-top.
- Focus management or browser behavior takes over. Browsers may move the viewport when focus changes, especially around interactive elements, inputs, or anchor targets.
- Hash navigation behaves differently. Linking to a URL with a fragment like
#sectiontriggers anchor scrolling independently ofscroll={false}. - Custom effects are scrolling manually. Code such as
window.scrollTo(0, 0),element.scrollIntoView(), or a route-change effect can override the expected behavior. - Navigation changes search params or dynamic segments. In some setups, the route update causes fresh rendering that makes the page appear to reset even though Next.js did not explicitly scroll.
In practice, the issue is usually not that scroll={false} is unsupported. The issue is that developers expect it to preserve all visual scroll state, while it only disables one specific part of Next.js navigation behavior.
Step-by-Step Solution
The safest fix is to first identify what is actually scrolling, then preserve that state explicitly if needed.
1. Confirm you are using client-side navigation correctly
Make sure the navigation is happening through Next.js Link, not a normal anchor or a full page reload.
import Link from 'next/link'
export default function Example() {
return (
<Link href="/products?tab=details" scroll={false}>
Open details
</Link>
)
}
If this becomes a full document navigation, scroll={false} will not help.
2. Check whether the page scrolls on window or inside a container
If your app uses a custom scrolling container, Next.js cannot preserve that container automatically.
export default function Layout({ children }) {
return (
<div className="app-shell">
<aside>Sidebar</aside>
<main className="scroll-area">{children}</main>
</div>
)
}
.scroll-area {
height: 100vh;
overflow: auto;
}
In this case, the visible scroll is on .scroll-area, not on window. You must preserve it yourself.
3. Persist and restore the custom scroll container manually
If your route changes within a scrollable panel, store the scroll position before navigation and restore it after render.
'use client'
import Link from 'next/link'
import { useEffect, useRef } from 'react'
import { usePathname, useSearchParams } from 'next/navigation'
export default function ScrollContainer({ children }) {
const ref = useRef(null)
const pathname = usePathname()
const searchParams = useSearchParams()
const key = `scroll:${pathname}?${searchParams.toString()}`
useEffect(() => {
const el = ref.current
if (!el) return
const saved = sessionStorage.getItem(key)
if (saved) {
el.scrollTop = Number(saved)
}
}, [key])
useEffect(() => {
const el = ref.current
if (!el) return
const onScroll = () => {
sessionStorage.setItem(key, String(el.scrollTop))
}
el.addEventListener('scroll', onScroll)
return () => el.removeEventListener('scroll', onScroll)
}, [key])
return <main ref={ref} className="scroll-area">{children}</main>
}
Then use standard navigation:
<Link href="/feed?page=2" scroll={false}>Next page</Link>
This combination works because Next.js stops its own top-scroll behavior, and your app restores the real scroll container.
4. Avoid accidental manual scrolling in effects
Search your app for code like this:
useEffect(() => {
window.scrollTo(0, 0)
}, [pathname])
Or this:
someRef.current?.scrollIntoView()
Any of these will make scroll={false} appear ineffective.
5. Keep shared UI in a stable layout when possible
If a parent layout is recreated during navigation, the browser can visually jump because the entire subtree changes. In the App Router, keep persistent UI such as sidebars, tab shells, and scrollable panels in a shared layout.js so they do not unmount between related routes.
// app/dashboard/layout.js
export default function DashboardLayout({ children }) {
return (
<section>
<nav>Persistent navigation</nav>
{children}
</section>
)
}
This reduces remounting and makes scroll preservation more predictable.
6. For query-string updates, consider router navigation carefully
If the main goal is changing filters, tabs, or pagination without resetting position, use a client component and verify the route update is not rebuilding more UI than necessary.
'use client'
import { useRouter, usePathname, useSearchParams } from 'next/navigation'
export default function Filters() {
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
function updateTab(tab) {
const params = new URLSearchParams(searchParams.toString())
params.set('tab', tab)
router.push(`${pathname}?${params.toString()}`, { scroll: false })
}
return (
<button onClick={() => updateTab('details')}>
Details
</button>
)
}
This mirrors the same intent as the Link prop and can be easier to debug during controlled UI updates.
7. Test with a minimal route pair
If the behavior is still inconsistent, build a tiny reproduction with:
- one tall page
- one
Link scroll={false} - no custom effects
- no nested overflow container
- no hash fragment
If that works, the problem is in app-specific layout, focus, or scroll logic rather than in the core Next.js routing feature.
Common Edge Cases
- Hash links:
<Link href="/docs#api" scroll={false}>can still jump because the browser scrolls to the anchor target. - Modal routes and intercepting routes: if opening a route changes focus or mounts a new layer, the viewport may shift unexpectedly.
- Virtualized lists: libraries like react-window or react-virtualized often manage their own scroll state, so preserving position requires library-specific handling.
- Suspense and streaming: delayed content can change layout height after navigation, making the page appear to move even if initial scroll was preserved.
- Browser back/forward behavior: browsers have their own history scroll restoration rules, which can differ from push navigation.
- Sticky headers: if content reflows under a sticky element, users may interpret the layout shift as a scroll reset.
- Autofocus inputs: an input receiving focus after navigation can cause the viewport to reposition, especially on mobile browsers.
FAQ
Does scroll={false} preserve scroll for every layout and container?
No. It only disables Next.js automatic window scroll reset during navigation. If your app scrolls inside a custom container, you need to preserve that container’s scroll position yourself.
Why does it work on one route but not another?
Usually because the two routes do not share the same rendering structure. One route may keep the parent layout mounted, while another remounts key elements, triggers focus changes, or uses different overflow containers.
Should I use router.push(..., { scroll: false }) instead of Link scroll={false}?
They solve the same core problem. Use Link for normal navigation and router.push for programmatic updates like filters or tabs. If both seem broken, check for manual scrolling code, hash fragments, and custom scroll containers.
The key takeaway is simple: scroll={false} is not a full scroll-state preservation system. It only stops Next.js from performing its default top-of-page scroll on navigation. If your UI relies on nested scroll regions, remount-heavy layouts, or route-driven state changes, the durable fix is to preserve scroll intentionally at the component or layout level.