diff --git a/e2e/react-router/basic-scroll-restoration/src/main.tsx b/e2e/react-router/basic-scroll-restoration/src/main.tsx
index 8bc4c54c1c5..8478a83c0cb 100644
--- a/e2e/react-router/basic-scroll-restoration/src/main.tsx
+++ b/e2e/react-router/basic-scroll-restoration/src/main.tsx
@@ -20,22 +20,39 @@ const rootRoute = createRootRoute({
function RootComponent() {
return (
<>
-
-
- Home
- {' '}
-
- About
-
-
- About (No Reset)
-
-
- By-Element
-
+
+
+
+
+ Home
+ {' '}
+
+ About
+
+
+ About (No Reset)
+
+
+ By-Element
+
+
+
+
-
-
>
)
}
@@ -268,6 +285,7 @@ const router = createRouter({
defaultPreload: 'intent',
scrollRestoration: true,
getScrollRestorationKey: (location) => location.pathname,
+ scrollToTopSelectors: ['#sidebar'],
})
declare global {
diff --git a/e2e/react-router/basic-scroll-restoration/tests/app.spec.ts b/e2e/react-router/basic-scroll-restoration/tests/app.spec.ts
index c7360ce8a4a..9b3490860ee 100644
--- a/e2e/react-router/basic-scroll-restoration/tests/app.spec.ts
+++ b/e2e/react-router/basic-scroll-restoration/tests/app.spec.ts
@@ -115,3 +115,107 @@ test('scroll to top when not scrolled, regression test for #4782', async ({
const restoredScrollPosition = await page.evaluate(() => window.scrollY)
expect(restoredScrollPosition).toBe(0)
})
+
+test('resetScroll=false saves scroll position for back navigation without scroll event, regression test for #6595', async ({
+ page,
+}) => {
+ const storageKey = 'tsr-scroll-restoration-v1_3'
+ const targetScrollPosition = 1000
+
+ await page.goto('/')
+ await expect(page.locator('#greeting')).toContainText('Welcome Home!')
+
+ await page.evaluate(
+ (scrollPos: number) => window.scrollTo(0, scrollPos),
+ targetScrollPosition,
+ )
+
+ await page.waitForFunction(
+ ([key, path, expectedY]) => {
+ const cache = sessionStorage.getItem(key)
+ if (!cache) return false
+ const parsed = JSON.parse(cache)
+ return parsed[path]?.window?.scrollY === expectedY
+ },
+ [storageKey, '/', targetScrollPosition] as const,
+ )
+
+ const scrollBeforeNav = await page.evaluate(() => window.scrollY)
+ expect(scrollBeforeNav).toBe(targetScrollPosition)
+
+ await page.getByRole('link', { name: 'About (No Reset)' }).click()
+ await expect(page.locator('#greeting')).toContainText('Hello from About!')
+
+ await page.waitForFunction(
+ ([key, path, expectedY]) => {
+ const cache = sessionStorage.getItem(key)
+ if (!cache) return false
+ const parsed = JSON.parse(cache)
+ return parsed[path]?.window?.scrollY === expectedY
+ },
+ [storageKey, '/about', targetScrollPosition] as const,
+ )
+
+ await page.goto('/foo')
+ await expect(page.getByTestId('foo-route-component')).toBeVisible()
+
+ await page.goBack()
+ await expect(page.locator('#greeting')).toContainText('Hello from About!')
+
+ await page.waitForFunction(
+ (expectedY) => window.scrollY === expectedY,
+ targetScrollPosition,
+ )
+
+ const restoredScrollPosition = await page.evaluate(() => window.scrollY)
+ expect(restoredScrollPosition).toBe(targetScrollPosition)
+})
+
+test('resetScroll=false preserves scrollToTopSelectors element scroll position, extension of #6595', async ({
+ page,
+}) => {
+ const storageKey = 'tsr-scroll-restoration-v1_3'
+ const sidebarScrollPosition = 500
+
+ await page.goto('/')
+ await expect(page.locator('#greeting')).toContainText('Welcome Home!')
+
+ await page.evaluate((scrollPos: number) => {
+ const sidebar = document.querySelector('#sidebar')
+ if (sidebar) sidebar.scrollTo(0, scrollPos)
+ }, sidebarScrollPosition)
+
+ const sidebarScrollBeforeNav = await page.evaluate(
+ () => document.querySelector('#sidebar')?.scrollTop,
+ )
+ expect(sidebarScrollBeforeNav).toBe(sidebarScrollPosition)
+
+ await page.getByRole('link', { name: 'About (No Reset)' }).click()
+ await expect(page.locator('#greeting')).toContainText('Hello from About!')
+
+ await page.waitForFunction(
+ ([key, path, expectedY]) => {
+ const cache = sessionStorage.getItem(key)
+ if (!cache) return false
+ const parsed = JSON.parse(cache)
+ return parsed[path]?.['#sidebar']?.scrollY === expectedY
+ },
+ [storageKey, '/about', sidebarScrollPosition] as const,
+ )
+
+ await page.goto('/foo')
+ await expect(page.getByTestId('foo-route-component')).toBeVisible()
+
+ await page.goBack()
+ await expect(page.locator('#greeting')).toContainText('Hello from About!')
+
+ await page.waitForFunction(
+ (expectedY) => document.querySelector('#sidebar')?.scrollTop === expectedY,
+ sidebarScrollPosition,
+ )
+
+ const restoredSidebarScroll = await page.evaluate(
+ () => document.querySelector('#sidebar')?.scrollTop,
+ )
+ expect(restoredSidebarScroll).toBe(sidebarScrollPosition)
+})
diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts
index 6520f307994..369d8063758 100644
--- a/packages/router-core/src/router.ts
+++ b/packages/router-core/src/router.ts
@@ -30,7 +30,12 @@ import {
} from './path'
import { createLRUCache } from './lru-cache'
import { isNotFound } from './not-found'
-import { setupScrollRestoration } from './scroll-restoration'
+import {
+ defaultGetScrollRestorationKey,
+ getCssSelector,
+ scrollRestorationCache,
+ setupScrollRestoration,
+} from './scroll-restoration'
import { defaultParseSearch, defaultStringifySearch } from './searchParams'
import { rootRouteId } from './root'
import { isRedirect, redirect } from './redirect'
@@ -41,6 +46,7 @@ import {
executeRewriteOutput,
rewriteBasepath,
} from './rewrite'
+import type { ScrollRestorationByElement } from './scroll-restoration'
import type { LRUCache } from './lru-cache'
import type {
ProcessRouteTreeResult,
@@ -2127,6 +2133,42 @@ export class RouterCore<
this.shouldViewTransition = viewTransition
+ if (
+ next.resetScroll === false &&
+ this.isScrollRestoring &&
+ scrollRestorationCache
+ ) {
+ const getKey =
+ this.options.getScrollRestorationKey || defaultGetScrollRestorationKey
+ const toKey = getKey(next as unknown as ParsedLocation)
+ scrollRestorationCache.set((state) => {
+ const keyEntry = (state[toKey] ||= {} as ScrollRestorationByElement)
+ keyEntry['window'] = {
+ scrollX: window.scrollX || 0,
+ scrollY: window.scrollY || 0,
+ }
+ if (this.options.scrollToTopSelectors) {
+ for (const selector of this.options.scrollToTopSelectors) {
+ const element =
+ typeof selector === 'function'
+ ? selector()
+ : document.querySelector(selector)
+ if (element) {
+ const elementSelector =
+ typeof selector === 'string'
+ ? selector
+ : getCssSelector(element)
+ keyEntry[elementSelector] = {
+ scrollX: element.scrollLeft || 0,
+ scrollY: element.scrollTop || 0,
+ }
+ }
+ }
+ }
+ return state
+ })
+ }
+
this.history[next.replace ? 'replace' : 'push'](
nextHistory.publicHref,
nextHistory.state,