By default, every modern browser remembers where you were scrolled on a page and restores that position on refresh. Usually that is what you want. Sometimes it is not, long pages with above-the-fold marketing content, single-page-app routes that share a URL, dashboards where "refresh" means "show me the top of the new data". The fix is one line of JavaScript, but there are five ways to write it and three of them have subtle pitfalls. This is the full reference: classic APIs, the modern history.scrollRestoration opt-out, the React Router and Next.js patterns, the hydration race that makes naive implementations flaky in SPAs, and the browser-compatibility footnotes that matter.
How do I force a page to scroll to the top on browser refresh?
The minimum viable line is window.scrollTo(0, 0) in a DOMContentLoaded or load listener, but on a modern SPA you almost always want to disable the browser's automatic scroll restoration first: if ('scrollRestoration' in history) history.scrollRestoration = 'manual';. That stops the browser from re-applying the previous scroll position on back-forward navigation and refresh, so your own scroll logic actually wins. For React Router 6.4+ projects, drop <ScrollRestoration getKey={(location) => location.pathname} /> into your root layout. For Next.js App Router projects, scrolling to top on route change is the default, pass scroll={false} on <Link> only when you want to opt out. Run the scroll call after hydration to avoid React clobbering the position mid-mount.
Jump to:
- Why browsers restore scroll position by default
- Classic: window.scrollTo on load
- Smooth-scroll variant
- The modern way: history.scrollRestoration
- React Router 6.4+ ScrollRestoration
- Next.js: scroll behavior on Link
- The hydration race in SPAs
- Saving and restoring scroll yourself
- Browser compatibility
- Common pitfalls
- What to do next
- FAQ
Why browsers restore scroll position by default
Every browser since around 2014 remembers your scroll position on:
- Reload (Cmd-R / Ctrl-R / F5)
- Back / Forward navigation (Alt-Left / browser back button)
- Restoring a tab from history (Cmd-Shift-T)
This is a usability feature: a long article you reload mid-read should not throw you back to the top. The behavior is governed by history.scrollRestoration, which defaults to 'auto'. Setting it to 'manual' opts your page out and gives your JavaScript full control.
The catch: in single-page apps, the browser does not know which "page" the URL represents because the URL changes without a full document reload. The framework router has to wire in its own scroll behavior, and historically every router rolled its own. Modern routers (React Router 6.4+, Next.js App Router) now ship with sane defaults that you can tune.
Classic: window.scrollTo on load
The most direct approach, no API negotiation:
window.addEventListener('load', () => {
window.scrollTo(0, 0);
});window.scrollTo(x, y) jumps to absolute pixel coordinates. (0, 0) is the top-left of the page. The load event fires after all resources (images, stylesheets) are loaded, late but reliable. If you want it earlier, use DOMContentLoaded:
document.addEventListener('DOMContentLoaded', () => {
window.scrollTo(0, 0);
});DOMContentLoaded fires after the HTML is parsed and the DOM is ready, before sub-resources finish loading. For most pages this is the right moment, the user does not see the document until the renderer has the layout.
The pitfall: if the browser has its own scroll restoration enabled (the default), it will re-apply the saved scroll position after DOMContentLoaded. Your scrollTo(0, 0) runs, then the browser overrides it. This is why the modern recommendation is to disable history.scrollRestoration first (see below).
Smooth-scroll variant
For an animated scroll instead of an instant jump:
window.scrollTo({
top: 0,
left: 0,
behavior: 'smooth'
});The object-form of scrollTo accepts { top, left, behavior }. behavior: 'smooth' uses the browser's native smooth-scroll animation (no library needed). 'instant' is the default; 'auto' defers to the page's scroll-behavior CSS property.
On page refresh, an animated scroll is usually wrong, the user sees the page paint at one position, then visibly scrolls. Reserve 'smooth' for in-page anchor links and "back to top" buttons. For refresh, use the instant form or just scrollTo(0, 0).
Note: setting scroll-behavior: smooth on the html element in CSS makes every scroll animated, including scrollTo(0, 0). If you want both, smooth in-page anchors and instant on refresh, pass behavior: 'instant' explicitly on the refresh path.
The modern way: history.scrollRestoration
Standardized in HTML Living Standard, supported in every browser since 2016. This is the right primary tool in 2026:
if ('scrollRestoration' in history) {
history.scrollRestoration = 'manual';
}
window.scrollTo(0, 0);The 'manual' value tells the browser: "do not restore scroll position automatically, I will handle it." Setting it once at page load is enough; the value persists for the session for that history entry.
This is the right call for most landing pages and dashboards. It also avoids the race where your scrollTo(0, 0) runs and then the browser scrolls you back to wherever you were before.
For a "always start at top" landing page, the entire script is two lines:
history.scrollRestoration = 'manual';
window.scrollTo(0, 0);Put that in a <script> tag in <head> (before any other JavaScript) or in the entry point of your bundle.
If you want the browser's smart default for most navigation but a forced top on full reload specifically, check performance.getEntriesByType('navigation')[0].type:
const nav = performance.getEntriesByType('navigation')[0];
if (nav && nav.type === 'reload') {
history.scrollRestoration = 'manual';
window.scrollTo(0, 0);
}That preserves back/forward scroll memory but resets on F5. Useful for content pages where back/forward should feel natural but a deliberate refresh should signal "show me from the top."
React Router 6.4+ ScrollRestoration
React Router shipped a built-in scroll restoration component in 6.4. Drop it into your root layout:
import { ScrollRestoration } from 'react-router-dom';
export default function RootLayout() {
return (
<>
<Outlet />
<ScrollRestoration />
</>
);
}
By default this scrolls to top on every navigation. To customize per-route (e.g. preserve scroll within tabbed views that share a path), use the getKey prop:
<ScrollRestoration getKey={(location) => location.pathname} />
The getKey function returns a unique identifier for each "scrollable view". If two locations return the same key, React Router preserves the scroll position; if different, it scrolls to top.
For more control, the getKey can return a custom key that you compute. The official docs at React Router Scroll Restoration cover the edge cases.
Important: <ScrollRestoration /> sets history.scrollRestoration = 'manual' internally. Do not also set it manually elsewhere, the conflict produces flicker on navigation.
Next.js: scroll behavior on Link
Next.js handles scroll-to-top automatically on every route change in both the Pages Router and the App Router. The relevant prop is scroll on <Link>:
import Link from 'next/link';
// Default: scrolls to top on navigation
<Link href="/products">Products</Link>
// Opt out: preserve current scroll position
<Link href="/products" scroll={false}>Products</Link>
The default is scroll={true}, so most of the time you do not need to think about it. The cases where you want scroll={false}:
- Tab-style UI where clicking a tab updates the URL but keeps the user where they are.
- Filter/sort changes that update query params but should not move the page.
- Infinite-scroll pagination where the URL reflects the page but the scroll position should not jump.
For programmatic navigation, the useRouter hook's push and replace accept the same option:
import { useRouter } from 'next/navigation';
const router = useRouter();
router.push('/products', { scroll: false });
For full document reload behavior (browser refresh), Next.js delegates to the browser's history.scrollRestoration. If you want a "scroll to top on refresh" override in a Next.js app, the same modern recipe works, add it to a client component near the root.
The hydration race in SPAs
A common bug in React and similar frameworks: your window.scrollTo(0, 0) fires, but the page still ends up scrolled somewhere weird. The cause is the hydration race:
- Browser parses HTML, restores saved scroll position.
- Your
scrollTo(0, 0)runs and scrolls to top. - React hydration runs, mounts components, lays out content above the fold that was not in the SSR snapshot, and the browser re-anchors the scroll position relative to whatever was on screen.
The fix is to set history.scrollRestoration = 'manual' before hydration runs, and to scroll after the layout settles:
// In a top-level client component
'use client';
import { useEffect } from 'react';
export function ScrollToTop() {
useEffect(() => {
if ('scrollRestoration' in history) {
history.scrollRestoration = 'manual';
}
// Wait one frame so hydration-driven layout has settled
requestAnimationFrame(() => {
window.scrollTo(0, 0);
});
}, []);
return null;
}
The requestAnimationFrame defer is the small but important detail, it gives React one paint cycle to settle the DOM before you scroll. Without it, the scroll fires during reconciliation and the browser may overshoot or undershoot.
For Next.js specifically, set history.scrollRestoration in a script that runs before the framework bundle:
// app/layout.tsx
<head>
<script
dangerouslySetInnerHTML={{
__html: `if('scrollRestoration' in history) history.scrollRestoration='manual';`,
}}
/>
</head>
Inline <script> in <head> runs synchronously during HTML parsing, before any module bundle is fetched. That kills the browser's auto-restore before it has a chance to fire.
Saving and restoring scroll yourself
If you want full control, e.g. save scroll per route and restore it when the user navigates back, but reset on full reload, use sessionStorage and the beforeunload event:
// Save scroll on unload
window.addEventListener('beforeunload', () => {
sessionStorage.setItem('scroll:' + location.pathname, String(window.scrollY));
});
// Restore on load
window.addEventListener('load', () => {
history.scrollRestoration = 'manual';
const saved = sessionStorage.getItem('scroll:' + location.pathname);
if (saved !== null) {
window.scrollTo(0, parseInt(saved, 10));
} else {
window.scrollTo(0, 0);
}
});A few notes:
beforeunloadis unreliable on mobile. Mobile Safari and Chrome may not firebeforeunloadwhen the tab is killed by the OS. The more reliable event ispagehide:
window.addEventListener('pagehide', () => {
sessionStorage.setItem('scroll:' + location.pathname, String(window.scrollY));
});sessionStorageis per-tab. If the user opens the same URL in a new tab, they get a fresh state. UselocalStorageif you want cross-tab persistence (rarely the right call for scroll position).- Save per-pathname, not per-URL. Query params usually represent the same scrollable view in a different state.
Browser compatibility
| Feature | Chrome | Firefox | Safari | Edge | First supported |
|---|---|---|---|---|---|
window.scrollTo(x, y) | All | All | All | All | Ancient |
window.scrollTo({ behavior }) | 61+ | 36+ | 14+ | 79+ | ~2018 |
history.scrollRestoration | 46+ | 46+ | 11+ | 79+ | ~2016 |
document.scrollingElement | 44+ | 48+ | 9+ | 79+ | ~2016 |
requestAnimationFrame | All | All | All | All | Ancient |
pagehide event | All | All | All | All | Ancient |
React Router <ScrollRestoration /> | n/a | n/a | n/a | n/a | React Router 6.4+ (Sept 2022) |
In 2026 you can use all of these without polyfills. The only thing to feature-detect is history.scrollRestoration, and that is more about being defensive than about real-world need.
Common pitfalls
1. Setting scrollTop on the wrong element. On older browsers, the scrolling root is either document.documentElement (standards mode) or document.body (quirks mode). The cross-browser-safe pattern is window.scrollTo(0, 0) or document.scrollingElement.scrollTop = 0. Touching document.body.scrollTop directly is unreliable.
2. CSS scroll-behavior: smooth overrides your behavior: 'instant'. If the html or body element has scroll-behavior: smooth in CSS, every scroll is animated regardless of what JS asks for. Pass behavior: 'instant' explicitly, or override the CSS scoped to the refresh path.
3. Calling scrollTo inside an iframe. Inside an iframe, window.scrollTo scrolls the iframe, not the parent page. To scroll the parent, you need window.top.scrollTo(0, 0), which the browser will only allow if same-origin.
4. Setting history.scrollRestoration after the browser has already restored. Set it as early as possible, ideally in a <script> in <head> before any framework code. Setting it inside a React useEffect is too late on the first paint.
5. React Router 6.3 and earlier do not have <ScrollRestoration />. If you are on an older version, either upgrade or roll your own scroll-on-route-change with useLocation and useEffect. The 6.4+ component is data-router-only, make sure your routes are set up with createBrowserRouter, not the older <BrowserRouter> JSX.
6. onbeforeunload returning a string used to show a confirm dialog. Browsers ignore the return value of beforeunload now; the only effect of setting it is whether the dialog fires. For scroll save/restore there is no need to return anything, just do the side effect.
7. Hash anchors override your scroll. If the URL ends in #section, the browser scrolls to that anchor element regardless of what your script asks. To force top, clear the hash first: history.replaceState(null, '', location.pathname + location.search).
What to do next
- How to Update Node.js Version, the toolchain refresh that usually pairs with adopting modern JS APIs like
scrollRestoration. - Bash For Loops, useful when scripting deploy steps for the JavaScript bundle that ships with this code.
- Bash While Loops, handy for build-watch scripts.
- How to Increase PHP Memory Limit, if your front-end JS is paired with a PHP backend, this is the next bottleneck you will hit during local dev.
- Domain Directing and Virtual Host Setup for WAMP Server, relevant if you are testing scroll behavior across multiple local hostnames.
- External: the MDN reference on history.scrollRestoration is the authoritative spec.





