From b4ed513e1f1c4ddd2469ded1450bc19e14a2b19f Mon Sep 17 00:00:00 2001
From: Manuel Schiller <manuel.schiller@caligano.de>
Date: Wed, 20 May 2026 22:09:50 +0200
Subject: [PATCH 1/6] fix: fix hash scroll restoration precedence

---
 .changeset/brave-dingos-hope.md               |  8 +++
 .../scroll-restoration/src/router.tsx         |  7 +++
 .../src/routes/(tests)/hash-scroll-repro.tsx  | 18 ++++++
 .../tests/hash-scroll-repro.spec.ts           | 62 +++++++++++++++++++
 packages/react-router/src/Transitioner.tsx    | 10 +--
 packages/router-core/src/hash-scroll.ts       | 21 -------
 packages/router-core/src/index.ts             |  3 -
 packages/router-core/src/router.ts            | 30 ++++++++-
 .../src/scroll-restoration-inline.ts          | 23 +++----
 .../src/scroll-restoration-script/server.ts   | 11 +---
 .../router-core/src/scroll-restoration.ts     | 62 +++++++++----------
 packages/solid-router/src/Transitioner.tsx    | 10 +--
 packages/vue-router/src/Transitioner.tsx      | 10 +--
 13 files changed, 165 insertions(+), 110 deletions(-)
 create mode 100644 .changeset/brave-dingos-hope.md
 delete mode 100644 packages/router-core/src/hash-scroll.ts

diff --git a/.changeset/brave-dingos-hope.md b/.changeset/brave-dingos-hope.md
new file mode 100644
index 0000000000..826c01993c
--- /dev/null
+++ b/.changeset/brave-dingos-hope.md
@@ -0,0 +1,8 @@
+---
+'@tanstack/router-core': patch
+'@tanstack/react-router': patch
+'@tanstack/solid-router': patch
+'@tanstack/vue-router': patch
+---
+
+Fix hash navigation being overridden by stale scroll restoration entries.
diff --git a/e2e/react-start/scroll-restoration/src/router.tsx b/e2e/react-start/scroll-restoration/src/router.tsx
index fef35c9e06..81ffc07ad3 100644
--- a/e2e/react-start/scroll-restoration/src/router.tsx
+++ b/e2e/react-start/scroll-restoration/src/router.tsx
@@ -7,6 +7,13 @@ export function getRouter() {
   const router = createRouter({
     routeTree,
     scrollRestoration: true,
+    getScrollRestorationKey: (location) => {
+      if (location.pathname === '/hash-scroll-repro') {
+        return location.pathname
+      }
+
+      return location.state.__TSR_key! || location.href
+    },
     defaultPreload: 'intent',
     defaultErrorComponent: DefaultCatchBoundary,
     defaultNotFoundComponent: () => <NotFound />,
diff --git a/e2e/react-start/scroll-restoration/src/routes/(tests)/hash-scroll-repro.tsx b/e2e/react-start/scroll-restoration/src/routes/(tests)/hash-scroll-repro.tsx
index ff85d140fa..31970306bd 100644
--- a/e2e/react-start/scroll-restoration/src/routes/(tests)/hash-scroll-repro.tsx
+++ b/e2e/react-start/scroll-restoration/src/routes/(tests)/hash-scroll-repro.tsx
@@ -44,9 +44,27 @@ function Component() {
           >
             Invalidate
           </button>
+          <Link
+            to="/hash-scroll-repro"
+            hash="one"
+            className="rounded border px-3 py-2"
+            data-testid="hash-scroll-section-one-link"
+          >
+            #one
+          </Link>
         </div>
       </div>
 
+      <div
+        data-scroll-restoration-id="hash-scroll-nested"
+        data-testid="hash-scroll-nested"
+        className="mt-4 h-24 overflow-auto rounded border p-2"
+      >
+        {Array.from({ length: 20 }).map((_, i) => (
+          <div key={i}>Nested scroll row {i}</div>
+        ))}
+      </div>
+
       <div className="mt-6 grid gap-10">
         {sectionIds.map((sectionId) => (
           <section
diff --git a/e2e/react-start/scroll-restoration/tests/hash-scroll-repro.spec.ts b/e2e/react-start/scroll-restoration/tests/hash-scroll-repro.spec.ts
index 3a8dc92afc..65dcf7a4dc 100644
--- a/e2e/react-start/scroll-restoration/tests/hash-scroll-repro.spec.ts
+++ b/e2e/react-start/scroll-restoration/tests/hash-scroll-repro.spec.ts
@@ -79,3 +79,65 @@ test('router.invalidate does not scroll back to the current hash', async ({
   const scrollYAfterInvalidate = await page.evaluate(() => window.scrollY)
   expect(scrollYAfterInvalidate).toBe(scrollYBeforeInvalidate)
 })
+
+test('hash navigation wins over stale same-tab scroll restoration entries', async ({
+  page,
+}) => {
+  await goToRepro(page)
+  const staleScrollY = await scrollUpFromHashTarget(page)
+
+  await page.reload()
+  await page.waitForLoadState('networkidle')
+  await expect(
+    page.getByTestId('hash-scroll-repro-invalidate-count'),
+  ).toBeVisible()
+
+  await page.getByTestId('hash-scroll-section-one-link').click()
+  await expect(page.getByTestId('hash-scroll-section-one')).toBeInViewport()
+
+  await expect(
+    page.getByTestId('hash-scroll-section-five'),
+  ).not.toBeInViewport()
+
+  const scrollYAfterHashNavigation = await page.evaluate(() => window.scrollY)
+  expect(scrollYAfterHashNavigation).toBeLessThan(staleScrollY)
+})
+
+test('hash navigation still runs when only nested scroll entries restore', async ({
+  page,
+}) => {
+  await goToRepro(page)
+
+  const nestedScrollTop = await page.evaluate(() => {
+    const nested = document.querySelector('[data-testid="hash-scroll-nested"]')
+    if (!(nested instanceof HTMLElement)) {
+      throw new Error('Missing nested scroller')
+    }
+
+    nested.scrollTop = 80
+    window.dispatchEvent(new PageTransitionEvent('pagehide'))
+    return nested.scrollTop
+  })
+
+  expect(nestedScrollTop).toBeGreaterThan(0)
+
+  await page.reload()
+  await page.waitForLoadState('networkidle')
+  await expect(
+    page.getByTestId('hash-scroll-repro-invalidate-count'),
+  ).toBeVisible()
+
+  await page.getByTestId('hash-scroll-section-one-link').click()
+  await expect(page.getByTestId('hash-scroll-section-one')).toBeInViewport()
+
+  await expect
+    .poll(async () => {
+      return page.evaluate(() => {
+        const nested = document.querySelector(
+          '[data-testid="hash-scroll-nested"]',
+        )
+        return nested instanceof HTMLElement ? nested.scrollTop : 0
+      })
+    })
+    .toBe(nestedScrollTop)
+})
diff --git a/packages/react-router/src/Transitioner.tsx b/packages/react-router/src/Transitioner.tsx
index 01abedded9..bf945571a6 100644
--- a/packages/react-router/src/Transitioner.tsx
+++ b/packages/react-router/src/Transitioner.tsx
@@ -2,11 +2,7 @@
 
 import * as React from 'react'
 import { batch, useStore } from '@tanstack/react-store'
-import {
-  getLocationChangeInfo,
-  handleHashScroll,
-  trimPathRight,
-} from '@tanstack/router-core'
+import { getLocationChangeInfo, trimPathRight } from '@tanstack/router-core'
 import { useLayoutEffect, usePrevious } from './utils'
 import { useRouter } from './useRouter'
 
@@ -128,10 +124,6 @@ export function Transitioner() {
         router.stores.status.set('idle')
         router.stores.resolvedLocation.set(router.stores.location.get())
       })
-
-      if (changeInfo.hrefChanged) {
-        handleHashScroll(router)
-      }
     }
   }, [isAnyPending, previousIsAnyPending, router])
 
diff --git a/packages/router-core/src/hash-scroll.ts b/packages/router-core/src/hash-scroll.ts
deleted file mode 100644
index ed8354890b..0000000000
--- a/packages/router-core/src/hash-scroll.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import type { AnyRouter } from './router'
-
-/**
- * @private
- * Handles hash-based scrolling after navigation completes.
- * To be used in framework-specific <Transitioner> components during the onResolved event.
- */
-export function handleHashScroll(router: AnyRouter) {
-  if (typeof document !== 'undefined' && (document as any).querySelector) {
-    const location = router.stores.location.get()
-    const hashScrollIntoViewOptions =
-      location.state.__hashScrollIntoViewOptions ?? true
-
-    if (hashScrollIntoViewOptions && location.hash !== '') {
-      const el = document.getElementById(location.hash)
-      if (el) {
-        el.scrollIntoView(hashScrollIntoViewOptions)
-      }
-    }
-  }
-}
diff --git a/packages/router-core/src/index.ts b/packages/router-core/src/index.ts
index e737fa50f8..b2929420a4 100644
--- a/packages/router-core/src/index.ts
+++ b/packages/router-core/src/index.ts
@@ -407,12 +407,9 @@ export {
   defaultGetScrollRestorationKey,
   getElementScrollRestorationEntry,
   storageKey,
-  scrollRestorationCache,
   setupScrollRestoration,
 } from './scroll-restoration'
 
-export { handleHashScroll } from './hash-scroll'
-
 export type {
   ScrollRestorationOptions,
   ScrollRestorationEntry,
diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts
index 3fbcd91387..03a7806f03 100644
--- a/packages/router-core/src/router.ts
+++ b/packages/router-core/src/router.ts
@@ -52,6 +52,7 @@ import type {
 import type { SearchParser, SearchSerializer } from './searchParams'
 import type { AnyRedirect, ResolvedRedirect } from './redirect'
 import type {
+  HistoryAction,
   HistoryLocation,
   HistoryState,
   ParsedHistoryState,
@@ -740,7 +741,10 @@ export type GetMatchRoutesFn = (pathname: string) => {
 
 export type EmitFn = (routerEvent: RouterEvent) => void
 
-export type LoadFn = (opts?: { sync?: boolean }) => Promise<void>
+export type LoadFn = (opts?: {
+  sync?: boolean
+  action?: { type: HistoryAction }
+}) => Promise<void>
 
 export type CommitLocationFn = ({
   viewTransition,
@@ -883,6 +887,20 @@ export function getLocationChangeInfo(
   return { fromLocation, toLocation, pathChanged, hrefChanged, hashChanged }
 }
 
+/**
+ * Symbol-keyed slot on ParsedLocation carrying the HistoryAction that
+ * triggered the current load. Read by the scroll-restoration listener to
+ * decide whether to skip stale window scroll restoration in favor of hash
+ * navigation. Symbol keys are excluded from Object.keys/JSON/spread, so this
+ * is invisible to user code.
+ * @private
+ */
+export const historyActionKey: unique symbol = Symbol()
+
+export type ParsedLocationWithHistoryAction = ParsedLocation & {
+  [historyActionKey]?: HistoryAction
+}
+
 export type CreateRouterFn = <
   TRouteTree extends AnyRoute,
   TTrailingSlashOption extends TrailingSlashOption = 'never',
@@ -2403,7 +2421,8 @@ export class RouterCore<
     })
   }
 
-  load: LoadFn = async (opts?: { sync?: boolean }): Promise<void> => {
+  load: LoadFn = async (opts): Promise<void> => {
+    const historyAction = opts?.action?.type
     let redirect: AnyRedirect | undefined
     let notFound: NotFoundError | undefined
     let loadPromise: Promise<void>
@@ -2415,6 +2434,13 @@ export class RouterCore<
       this.startTransition(async () => {
         try {
           this.beforeLoad()
+          // Stamp action onto the location instance via symbol key so downstream
+          // emitters of locationChangeInfo (e.g. onRendered from Match) can read
+          // it. Cleared on no-action loads (invalidate, same-URL commit, SSR
+          // hydration) since action describes the load event, not the URL.
+          ;(this.latestLocation as ParsedLocationWithHistoryAction)[
+            historyActionKey
+          ] = historyAction
           const next = this.latestLocation
           const prevLocation = this.stores.resolvedLocation.get()
           const locationChangeInfo = getLocationChangeInfo(next, prevLocation)
diff --git a/packages/router-core/src/scroll-restoration-inline.ts b/packages/router-core/src/scroll-restoration-inline.ts
index b6c772fa69..1e0328a663 100644
--- a/packages/router-core/src/scroll-restoration-inline.ts
+++ b/packages/router-core/src/scroll-restoration-inline.ts
@@ -1,9 +1,4 @@
-export default function (options: {
-  storageKey: string
-  key?: string
-  behavior?: ScrollToOptions['behavior']
-  shouldScrollRestoration?: boolean
-}) {
+export default function (options: { storageKey: string; key?: string }) {
   let byKey
 
   try {
@@ -15,13 +10,9 @@ export default function (options: {
 
   const resolvedKey = options.key || window.history.state?.__TSR_key
   const elementEntries = resolvedKey ? byKey[resolvedKey] : undefined
+  let windowRestored = false
 
-  if (
-    options.shouldScrollRestoration &&
-    elementEntries &&
-    typeof elementEntries === 'object' &&
-    Object.keys(elementEntries).length > 0
-  ) {
+  if (elementEntries && typeof elementEntries === 'object') {
     for (const elementSelector in elementEntries) {
       const entry = elementEntries[elementSelector]
 
@@ -40,8 +31,8 @@ export default function (options: {
         window.scrollTo({
           top: scrollY,
           left: scrollX,
-          behavior: options.behavior,
         })
+        windowRestored = true
       } else if (elementSelector) {
         let element
 
@@ -57,10 +48,10 @@ export default function (options: {
         }
       }
     }
-
-    return
   }
 
+  if (windowRestored) return
+
   const hash = window.location.hash.split('#', 2)[1]
 
   if (hash) {
@@ -77,5 +68,5 @@ export default function (options: {
     return
   }
 
-  window.scrollTo({ top: 0, left: 0, behavior: options.behavior })
+  window.scrollTo({ top: 0, left: 0 })
 }
diff --git a/packages/router-core/src/scroll-restoration-script/server.ts b/packages/router-core/src/scroll-restoration-script/server.ts
index cc7f0f38fb..d112f144b9 100644
--- a/packages/router-core/src/scroll-restoration-script/server.ts
+++ b/packages/router-core/src/scroll-restoration-script/server.ts
@@ -9,26 +9,18 @@ import type { AnyRouter } from '../router'
 type InlineScrollRestorationScriptOptions = {
   storageKey: string
   key?: string
-  behavior?: ScrollToOptions['behavior']
-  shouldScrollRestoration?: boolean
 }
 
 const defaultInlineScrollRestorationScript = `(${minifiedScrollRestorationScript})(${escapeHtml(
   JSON.stringify({
     storageKey,
-    shouldScrollRestoration: true,
   } satisfies InlineScrollRestorationScriptOptions),
 )})`
 
 function getScrollRestorationScript(
   options: InlineScrollRestorationScriptOptions,
 ) {
-  if (
-    options.storageKey === storageKey &&
-    options.shouldScrollRestoration === true &&
-    options.key === undefined &&
-    options.behavior === undefined
-  ) {
+  if (options.storageKey === storageKey && options.key === undefined) {
     return defaultInlineScrollRestorationScript
   }
 
@@ -58,7 +50,6 @@ export function getScrollRestorationScriptForRouter(router: AnyRouter) {
 
   return getScrollRestorationScript({
     storageKey,
-    shouldScrollRestoration: true,
     key: userKey,
   })
 }
diff --git a/packages/router-core/src/scroll-restoration.ts b/packages/router-core/src/scroll-restoration.ts
index a3a89ca1cb..d4ec57fbdb 100644
--- a/packages/router-core/src/scroll-restoration.ts
+++ b/packages/router-core/src/scroll-restoration.ts
@@ -1,6 +1,7 @@
 import { isServer } from '@tanstack/router-core/isServer'
 import { functionalUpdate, isPlainObject } from './utils'
-import type { AnyRouter } from './router'
+import { historyActionKey } from './router'
+import type { AnyRouter, ParsedLocationWithHistoryAction } from './router'
 import type { ParsedLocation } from './location'
 import type { NonNullableUpdater } from './utils'
 
@@ -76,7 +77,7 @@ function createScrollRestorationCache(): ScrollRestorationCache | null {
   }
 }
 
-export const scrollRestorationCache = createScrollRestorationCache()
+const scrollRestorationCache = createScrollRestorationCache()
 
 /**
  * The default `getKey` function for `useScrollRestoration`.
@@ -140,11 +141,7 @@ const scrollRestorationIdAttribute = 'data-scroll-restoration-id'
 type ScrollTarget = typeof windowScrollTarget | Element
 
 export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
-  if (!scrollRestorationCache && !(isServer ?? router.isServer)) {
-    return
-  }
-
-  const cache = scrollRestorationCache
+  // Keep hash/top scrolling active even when sessionStorage is unavailable.
 
   const shouldScrollRestoration =
     force ?? router.options.scrollRestoration ?? false
@@ -153,11 +150,7 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
     router.isScrollRestoring = true
   }
 
-  if (
-    (isServer ?? router.isServer) ||
-    router.isScrollRestorationSetup ||
-    !cache
-  ) {
+  if ((isServer ?? router.isServer) || router.isScrollRestorationSetup) {
     return
   }
 
@@ -195,12 +188,12 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
       !router.isScrollRestoring ||
       !restoreKey ||
       trackedScrollEntries.size === 0 ||
-      !cache
+      !scrollRestorationCache
     ) {
       return
     }
 
-    const keyEntry = (cache.state[restoreKey] ||=
+    const keyEntry = (scrollRestorationCache.state[restoreKey] ||=
       {} as ScrollRestorationByElement)
 
     for (const [target, position] of trackedScrollEntries) {
@@ -236,7 +229,7 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
         router.stores.resolvedLocation.get() ?? router.stores.location.get(),
       ),
     )
-    cache.persist()
+    scrollRestorationCache?.persist()
   })
 
   // Restore destination scroll after the new route has rendered.
@@ -261,10 +254,20 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
     ignoreScroll = true
 
     try {
+      const hash = event.toLocation.hash
+      const hashScrollIntoViewOptions =
+        event.toLocation.state.__hashScrollIntoViewOptions ?? true
+      const action = (event.toLocation as ParsedLocationWithHistoryAction)[
+        historyActionKey
+      ]
+      const skipWindowRestore =
+        hash &&
+        hashScrollIntoViewOptions &&
+        (action === 'PUSH' || action === 'REPLACE')
       const elementEntries = router.isScrollRestoring
-        ? cache.state[cacheKey]
+        ? scrollRestorationCache?.state[cacheKey]
         : undefined
-      let restored = false
+      let windowRestored = false
 
       if (elementEntries) {
         for (const elementSelector in elementEntries) {
@@ -284,12 +287,16 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
           }
 
           if (elementSelector === windowScrollTarget) {
+            if (skipWindowRestore) {
+              continue
+            }
+
             window.scrollTo({
               top: scrollY as number,
               left: scrollX as number,
               behavior,
             })
-            restored = true
+            windowRestored = true
           } else if (elementSelector) {
             let element
 
@@ -302,24 +309,17 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
             if (element) {
               element.scrollLeft = scrollX as number
               element.scrollTop = scrollY as number
-              restored = true
             }
           }
         }
       }
 
-      if (!restored) {
-        const hash = router.history.location.hash.slice(1)
-
+      if (!windowRestored) {
         if (hash) {
-          const hashScrollIntoViewOptions =
-            window.history.state?.__hashScrollIntoViewOptions ?? true
-
           if (hashScrollIntoViewOptions) {
-            const el = document.getElementById(hash)
-            if (el) {
-              el.scrollIntoView(hashScrollIntoViewOptions)
-            }
+            document
+              .getElementById(hash)
+              ?.scrollIntoView(hashScrollIntoViewOptions)
           }
         } else {
           const scrollOptions = {
@@ -347,8 +347,8 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
       ignoreScroll = false
     }
 
-    if (router.isScrollRestoring) {
-      cache.set((state) => {
+    if (router.isScrollRestoring && scrollRestorationCache) {
+      scrollRestorationCache.set((state) => {
         state[cacheKey] ||= {} as ScrollRestorationByElement
         return state
       })
diff --git a/packages/solid-router/src/Transitioner.tsx b/packages/solid-router/src/Transitioner.tsx
index d0983ba1ff..331aff5864 100644
--- a/packages/solid-router/src/Transitioner.tsx
+++ b/packages/solid-router/src/Transitioner.tsx
@@ -1,9 +1,5 @@
 import * as Solid from 'solid-js'
-import {
-  getLocationChangeInfo,
-  handleHashScroll,
-  trimPathRight,
-} from '@tanstack/router-core'
+import { getLocationChangeInfo, trimPathRight } from '@tanstack/router-core'
 import { isServer } from '@tanstack/router-core/isServer'
 import { useRouter } from './useRouter'
 
@@ -133,10 +129,6 @@ export function Transitioner() {
         router.stores.status.set('idle')
         router.stores.resolvedLocation.set(router.stores.location.get())
       })
-
-      if (changeInfo.hrefChanged) {
-        handleHashScroll(router)
-      }
     }
 
     return currentIsAnyPending
diff --git a/packages/vue-router/src/Transitioner.tsx b/packages/vue-router/src/Transitioner.tsx
index 66ebbfde38..b0133d965c 100644
--- a/packages/vue-router/src/Transitioner.tsx
+++ b/packages/vue-router/src/Transitioner.tsx
@@ -1,9 +1,5 @@
 import * as Vue from 'vue'
-import {
-  getLocationChangeInfo,
-  handleHashScroll,
-  trimPathRight,
-} from '@tanstack/router-core'
+import { getLocationChangeInfo, trimPathRight } from '@tanstack/router-core'
 import { isServer } from '@tanstack/router-core/isServer'
 import { batch, useStore } from '@tanstack/vue-store'
 import { useRouter } from './useRouter'
@@ -230,10 +226,6 @@ export function useTransitionerSetup() {
           type: 'onResolved',
           ...changeInfo,
         })
-
-        if (changeInfo.hrefChanged) {
-          handleHashScroll(router)
-        }
       }
     } catch {
       // Ignore errors if component is unmounted

From 7997a3f97d213a8b2f7e297724bfa9af45b138ef Mon Sep 17 00:00:00 2001
From: "nx-cloud[bot]" <71083854+nx-cloud[bot]@users.noreply.github.com>
Date: Wed, 20 May 2026 22:09:50 +0200
Subject: [PATCH 2/6] fix: only stamp historyActionKey on location when action
 is defined

---
 packages/router-core/src/router.ts | 14 +++++++++-----
 1 file changed, 9 insertions(+), 5 deletions(-)

diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts
index 03a7806f03..83d23eddbf 100644
--- a/packages/router-core/src/router.ts
+++ b/packages/router-core/src/router.ts
@@ -2436,11 +2436,15 @@ export class RouterCore<
           this.beforeLoad()
           // Stamp action onto the location instance via symbol key so downstream
           // emitters of locationChangeInfo (e.g. onRendered from Match) can read
-          // it. Cleared on no-action loads (invalidate, same-URL commit, SSR
-          // hydration) since action describes the load event, not the URL.
-          ;(this.latestLocation as ParsedLocationWithHistoryAction)[
-            historyActionKey
-          ] = historyAction
+          // it. Only set when an action is present; no-action loads (invalidate,
+          // same-URL commit, SSR hydration) leave the key absent so that
+          // deep-equality checks (e.g. toEqual) on location objects are not
+          // affected by an extraneous Symbol → undefined entry.
+          if (historyAction !== undefined) {
+            ;(this.latestLocation as ParsedLocationWithHistoryAction)[
+              historyActionKey
+            ] = historyAction
+          }
           const next = this.latestLocation
           const prevLocation = this.stores.resolvedLocation.get()
           const locationChangeInfo = getLocationChangeInfo(next, prevLocation)

From fba4db84e861d81af42f06184d6ed180b71e7cbf Mon Sep 17 00:00:00 2001
From: Manuel Schiller <manuel.schiller@caligano.de>
Date: Wed, 20 May 2026 22:09:50 +0200
Subject: [PATCH 3/6] fix: Preserve existing nested scroll positions across SPA
 navigations that create new restoration keys.

---
 .changeset/soft-badgers-sneeze.md             |   5 +
 .../scroll-restoration/src/routeTree.gen.ts   |  86 ++++++++
 .../scroll-restoration/src/router.tsx         |   1 +
 .../src/routes/(tests)/nested-scroll-away.tsx |  14 ++
 .../(tests)/nested-scroll-carry-over-a.tsx    |  49 +++++
 .../(tests)/nested-scroll-carry-over-b.tsx    |  38 ++++
 .../routes/(tests)/nested-scroll-search.tsx   |  47 +++++
 .../scroll-restoration/src/routes/index.tsx   |  14 ++
 .../tests/nested-scroll-carry-over.spec.ts    | 196 ++++++++++++++++++
 .../router-core/src/scroll-restoration.ts     | 146 ++++++++-----
 10 files changed, 548 insertions(+), 48 deletions(-)
 create mode 100644 .changeset/soft-badgers-sneeze.md
 create mode 100644 e2e/react-start/scroll-restoration/src/routes/(tests)/nested-scroll-away.tsx
 create mode 100644 e2e/react-start/scroll-restoration/src/routes/(tests)/nested-scroll-carry-over-a.tsx
 create mode 100644 e2e/react-start/scroll-restoration/src/routes/(tests)/nested-scroll-carry-over-b.tsx
 create mode 100644 e2e/react-start/scroll-restoration/src/routes/(tests)/nested-scroll-search.tsx
 create mode 100644 e2e/react-start/scroll-restoration/tests/nested-scroll-carry-over.spec.ts

diff --git a/.changeset/soft-badgers-sneeze.md b/.changeset/soft-badgers-sneeze.md
new file mode 100644
index 0000000000..cb97946ee9
--- /dev/null
+++ b/.changeset/soft-badgers-sneeze.md
@@ -0,0 +1,5 @@
+---
+'@tanstack/router-core': patch
+---
+
+Preserve existing nested scroll positions across SPA navigations that create new restoration keys.
diff --git a/e2e/react-start/scroll-restoration/src/routeTree.gen.ts b/e2e/react-start/scroll-restoration/src/routeTree.gen.ts
index 5194bbc556..07cb84ed0b 100644
--- a/e2e/react-start/scroll-restoration/src/routeTree.gen.ts
+++ b/e2e/react-start/scroll-restoration/src/routeTree.gen.ts
@@ -13,6 +13,10 @@ import { Route as IndexRouteImport } from './routes/index'
 import { Route as testsWithSearchRouteImport } from './routes/(tests)/with-search'
 import { Route as testsWithLoaderRouteImport } from './routes/(tests)/with-loader'
 import { Route as testsNormalPageRouteImport } from './routes/(tests)/normal-page'
+import { Route as testsNestedScrollSearchRouteImport } from './routes/(tests)/nested-scroll-search'
+import { Route as testsNestedScrollCarryOverBRouteImport } from './routes/(tests)/nested-scroll-carry-over-b'
+import { Route as testsNestedScrollCarryOverARouteImport } from './routes/(tests)/nested-scroll-carry-over-a'
+import { Route as testsNestedScrollAwayRouteImport } from './routes/(tests)/nested-scroll-away'
 import { Route as testsHashScrollReproRouteImport } from './routes/(tests)/hash-scroll-repro'
 import { Route as testsHashScrollAboutRouteImport } from './routes/(tests)/hash-scroll-about'
 
@@ -36,6 +40,28 @@ const testsNormalPageRoute = testsNormalPageRouteImport.update({
   path: '/normal-page',
   getParentRoute: () => rootRouteImport,
 } as any)
+const testsNestedScrollSearchRoute = testsNestedScrollSearchRouteImport.update({
+  id: '/(tests)/nested-scroll-search',
+  path: '/nested-scroll-search',
+  getParentRoute: () => rootRouteImport,
+} as any)
+const testsNestedScrollCarryOverBRoute =
+  testsNestedScrollCarryOverBRouteImport.update({
+    id: '/(tests)/nested-scroll-carry-over-b',
+    path: '/nested-scroll-carry-over-b',
+    getParentRoute: () => rootRouteImport,
+  } as any)
+const testsNestedScrollCarryOverARoute =
+  testsNestedScrollCarryOverARouteImport.update({
+    id: '/(tests)/nested-scroll-carry-over-a',
+    path: '/nested-scroll-carry-over-a',
+    getParentRoute: () => rootRouteImport,
+  } as any)
+const testsNestedScrollAwayRoute = testsNestedScrollAwayRouteImport.update({
+  id: '/(tests)/nested-scroll-away',
+  path: '/nested-scroll-away',
+  getParentRoute: () => rootRouteImport,
+} as any)
 const testsHashScrollReproRoute = testsHashScrollReproRouteImport.update({
   id: '/(tests)/hash-scroll-repro',
   path: '/hash-scroll-repro',
@@ -51,6 +77,10 @@ export interface FileRoutesByFullPath {
   '/': typeof IndexRoute
   '/hash-scroll-about': typeof testsHashScrollAboutRoute
   '/hash-scroll-repro': typeof testsHashScrollReproRoute
+  '/nested-scroll-away': typeof testsNestedScrollAwayRoute
+  '/nested-scroll-carry-over-a': typeof testsNestedScrollCarryOverARoute
+  '/nested-scroll-carry-over-b': typeof testsNestedScrollCarryOverBRoute
+  '/nested-scroll-search': typeof testsNestedScrollSearchRoute
   '/normal-page': typeof testsNormalPageRoute
   '/with-loader': typeof testsWithLoaderRoute
   '/with-search': typeof testsWithSearchRoute
@@ -59,6 +89,10 @@ export interface FileRoutesByTo {
   '/': typeof IndexRoute
   '/hash-scroll-about': typeof testsHashScrollAboutRoute
   '/hash-scroll-repro': typeof testsHashScrollReproRoute
+  '/nested-scroll-away': typeof testsNestedScrollAwayRoute
+  '/nested-scroll-carry-over-a': typeof testsNestedScrollCarryOverARoute
+  '/nested-scroll-carry-over-b': typeof testsNestedScrollCarryOverBRoute
+  '/nested-scroll-search': typeof testsNestedScrollSearchRoute
   '/normal-page': typeof testsNormalPageRoute
   '/with-loader': typeof testsWithLoaderRoute
   '/with-search': typeof testsWithSearchRoute
@@ -68,6 +102,10 @@ export interface FileRoutesById {
   '/': typeof IndexRoute
   '/(tests)/hash-scroll-about': typeof testsHashScrollAboutRoute
   '/(tests)/hash-scroll-repro': typeof testsHashScrollReproRoute
+  '/(tests)/nested-scroll-away': typeof testsNestedScrollAwayRoute
+  '/(tests)/nested-scroll-carry-over-a': typeof testsNestedScrollCarryOverARoute
+  '/(tests)/nested-scroll-carry-over-b': typeof testsNestedScrollCarryOverBRoute
+  '/(tests)/nested-scroll-search': typeof testsNestedScrollSearchRoute
   '/(tests)/normal-page': typeof testsNormalPageRoute
   '/(tests)/with-loader': typeof testsWithLoaderRoute
   '/(tests)/with-search': typeof testsWithSearchRoute
@@ -78,6 +116,10 @@ export interface FileRouteTypes {
     | '/'
     | '/hash-scroll-about'
     | '/hash-scroll-repro'
+    | '/nested-scroll-away'
+    | '/nested-scroll-carry-over-a'
+    | '/nested-scroll-carry-over-b'
+    | '/nested-scroll-search'
     | '/normal-page'
     | '/with-loader'
     | '/with-search'
@@ -86,6 +128,10 @@ export interface FileRouteTypes {
     | '/'
     | '/hash-scroll-about'
     | '/hash-scroll-repro'
+    | '/nested-scroll-away'
+    | '/nested-scroll-carry-over-a'
+    | '/nested-scroll-carry-over-b'
+    | '/nested-scroll-search'
     | '/normal-page'
     | '/with-loader'
     | '/with-search'
@@ -94,6 +140,10 @@ export interface FileRouteTypes {
     | '/'
     | '/(tests)/hash-scroll-about'
     | '/(tests)/hash-scroll-repro'
+    | '/(tests)/nested-scroll-away'
+    | '/(tests)/nested-scroll-carry-over-a'
+    | '/(tests)/nested-scroll-carry-over-b'
+    | '/(tests)/nested-scroll-search'
     | '/(tests)/normal-page'
     | '/(tests)/with-loader'
     | '/(tests)/with-search'
@@ -103,6 +153,10 @@ export interface RootRouteChildren {
   IndexRoute: typeof IndexRoute
   testsHashScrollAboutRoute: typeof testsHashScrollAboutRoute
   testsHashScrollReproRoute: typeof testsHashScrollReproRoute
+  testsNestedScrollAwayRoute: typeof testsNestedScrollAwayRoute
+  testsNestedScrollCarryOverARoute: typeof testsNestedScrollCarryOverARoute
+  testsNestedScrollCarryOverBRoute: typeof testsNestedScrollCarryOverBRoute
+  testsNestedScrollSearchRoute: typeof testsNestedScrollSearchRoute
   testsNormalPageRoute: typeof testsNormalPageRoute
   testsWithLoaderRoute: typeof testsWithLoaderRoute
   testsWithSearchRoute: typeof testsWithSearchRoute
@@ -138,6 +192,34 @@ declare module '@tanstack/react-router' {
       preLoaderRoute: typeof testsNormalPageRouteImport
       parentRoute: typeof rootRouteImport
     }
+    '/(tests)/nested-scroll-search': {
+      id: '/(tests)/nested-scroll-search'
+      path: '/nested-scroll-search'
+      fullPath: '/nested-scroll-search'
+      preLoaderRoute: typeof testsNestedScrollSearchRouteImport
+      parentRoute: typeof rootRouteImport
+    }
+    '/(tests)/nested-scroll-carry-over-b': {
+      id: '/(tests)/nested-scroll-carry-over-b'
+      path: '/nested-scroll-carry-over-b'
+      fullPath: '/nested-scroll-carry-over-b'
+      preLoaderRoute: typeof testsNestedScrollCarryOverBRouteImport
+      parentRoute: typeof rootRouteImport
+    }
+    '/(tests)/nested-scroll-carry-over-a': {
+      id: '/(tests)/nested-scroll-carry-over-a'
+      path: '/nested-scroll-carry-over-a'
+      fullPath: '/nested-scroll-carry-over-a'
+      preLoaderRoute: typeof testsNestedScrollCarryOverARouteImport
+      parentRoute: typeof rootRouteImport
+    }
+    '/(tests)/nested-scroll-away': {
+      id: '/(tests)/nested-scroll-away'
+      path: '/nested-scroll-away'
+      fullPath: '/nested-scroll-away'
+      preLoaderRoute: typeof testsNestedScrollAwayRouteImport
+      parentRoute: typeof rootRouteImport
+    }
     '/(tests)/hash-scroll-repro': {
       id: '/(tests)/hash-scroll-repro'
       path: '/hash-scroll-repro'
@@ -159,6 +241,10 @@ const rootRouteChildren: RootRouteChildren = {
   IndexRoute: IndexRoute,
   testsHashScrollAboutRoute: testsHashScrollAboutRoute,
   testsHashScrollReproRoute: testsHashScrollReproRoute,
+  testsNestedScrollAwayRoute: testsNestedScrollAwayRoute,
+  testsNestedScrollCarryOverARoute: testsNestedScrollCarryOverARoute,
+  testsNestedScrollCarryOverBRoute: testsNestedScrollCarryOverBRoute,
+  testsNestedScrollSearchRoute: testsNestedScrollSearchRoute,
   testsNormalPageRoute: testsNormalPageRoute,
   testsWithLoaderRoute: testsWithLoaderRoute,
   testsWithSearchRoute: testsWithSearchRoute,
diff --git a/e2e/react-start/scroll-restoration/src/router.tsx b/e2e/react-start/scroll-restoration/src/router.tsx
index 81ffc07ad3..0d529ba002 100644
--- a/e2e/react-start/scroll-restoration/src/router.tsx
+++ b/e2e/react-start/scroll-restoration/src/router.tsx
@@ -7,6 +7,7 @@ export function getRouter() {
   const router = createRouter({
     routeTree,
     scrollRestoration: true,
+    scrollToTopSelectors: ['[data-scroll-restoration-id="carry-over-reset"]'],
     getScrollRestorationKey: (location) => {
       if (location.pathname === '/hash-scroll-repro') {
         return location.pathname
diff --git a/e2e/react-start/scroll-restoration/src/routes/(tests)/nested-scroll-away.tsx b/e2e/react-start/scroll-restoration/src/routes/(tests)/nested-scroll-away.tsx
new file mode 100644
index 0000000000..82b43f8077
--- /dev/null
+++ b/e2e/react-start/scroll-restoration/src/routes/(tests)/nested-scroll-away.tsx
@@ -0,0 +1,14 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/(tests)/nested-scroll-away')({
+  component: Component,
+})
+
+function Component() {
+  return (
+    <div className="grid gap-4 p-2">
+      <h3>nested-scroll-away</h3>
+      <p>This route intentionally has no nested scroll container.</p>
+    </div>
+  )
+}
diff --git a/e2e/react-start/scroll-restoration/src/routes/(tests)/nested-scroll-carry-over-a.tsx b/e2e/react-start/scroll-restoration/src/routes/(tests)/nested-scroll-carry-over-a.tsx
new file mode 100644
index 0000000000..624727775c
--- /dev/null
+++ b/e2e/react-start/scroll-restoration/src/routes/(tests)/nested-scroll-carry-over-a.tsx
@@ -0,0 +1,49 @@
+import { Link, createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/(tests)/nested-scroll-carry-over-a')({
+  component: Component,
+})
+
+function ScrollBox({
+  restorationId,
+  testId,
+}: {
+  restorationId: string
+  testId: string
+}) {
+  return (
+    <div
+      data-scroll-restoration-id={restorationId}
+      data-testid={testId}
+      className="h-24 overflow-auto rounded border p-2"
+    >
+      {Array.from({ length: 20 }).map((_, i) => (
+        <div key={i}>Source row {i}</div>
+      ))}
+    </div>
+  )
+}
+
+function Component() {
+  return (
+    <div className="grid gap-4 p-2">
+      <h3>nested-scroll-carry-over-a</h3>
+      <Link
+        to="/nested-scroll-carry-over-b"
+        data-testid="nested-scroll-carry-over-link"
+      >
+        Go to target
+      </Link>
+
+      <ScrollBox
+        restorationId="carry-over-preserved"
+        testId="carry-over-preserved"
+      />
+      <ScrollBox restorationId="carry-over-reset" testId="carry-over-reset" />
+      <ScrollBox
+        restorationId="carry-over-source-only"
+        testId="carry-over-source-only"
+      />
+    </div>
+  )
+}
diff --git a/e2e/react-start/scroll-restoration/src/routes/(tests)/nested-scroll-carry-over-b.tsx b/e2e/react-start/scroll-restoration/src/routes/(tests)/nested-scroll-carry-over-b.tsx
new file mode 100644
index 0000000000..69dddb2d14
--- /dev/null
+++ b/e2e/react-start/scroll-restoration/src/routes/(tests)/nested-scroll-carry-over-b.tsx
@@ -0,0 +1,38 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/(tests)/nested-scroll-carry-over-b')({
+  component: Component,
+})
+
+function ScrollBox({
+  restorationId,
+  testId,
+}: {
+  restorationId: string
+  testId: string
+}) {
+  return (
+    <div
+      data-scroll-restoration-id={restorationId}
+      data-testid={testId}
+      className="h-24 overflow-auto rounded border p-2"
+    >
+      {Array.from({ length: 20 }).map((_, i) => (
+        <div key={i}>Target row {i}</div>
+      ))}
+    </div>
+  )
+}
+
+function Component() {
+  return (
+    <div className="grid gap-4 p-2">
+      <h3>nested-scroll-carry-over-b</h3>
+      <ScrollBox
+        restorationId="carry-over-preserved"
+        testId="carry-over-preserved"
+      />
+      <ScrollBox restorationId="carry-over-reset" testId="carry-over-reset" />
+    </div>
+  )
+}
diff --git a/e2e/react-start/scroll-restoration/src/routes/(tests)/nested-scroll-search.tsx b/e2e/react-start/scroll-restoration/src/routes/(tests)/nested-scroll-search.tsx
new file mode 100644
index 0000000000..c9889386f2
--- /dev/null
+++ b/e2e/react-start/scroll-restoration/src/routes/(tests)/nested-scroll-search.tsx
@@ -0,0 +1,47 @@
+import { Link, createFileRoute } from '@tanstack/react-router'
+import { z } from 'zod'
+import { zodValidator } from '@tanstack/zod-adapter'
+
+export const Route = createFileRoute('/(tests)/nested-scroll-search')({
+  validateSearch: zodValidator(
+    z.object({
+      query: z.string().optional(),
+    }),
+  ),
+  component: Component,
+})
+
+function Component() {
+  const search = Route.useSearch()
+  const query = search.query ?? 'none'
+
+  return (
+    <div className="grid gap-4 p-2">
+      <h3>nested-scroll-search</h3>
+      <p data-testid="nested-scroll-search-query">query: {query}</p>
+      <div className="flex gap-2">
+        <Link
+          to="/nested-scroll-search"
+          search={{ query: 'xyz' }}
+          data-testid="nested-scroll-search-link"
+        >
+          Set query
+        </Link>
+        <Link to="/nested-scroll-away" data-testid="nested-scroll-away-link">
+          Away
+        </Link>
+      </div>
+      <div
+        data-scroll-restoration-id="nested-scroll-search-container"
+        data-testid="nested-scroll-search-container"
+        className="h-24 overflow-auto rounded border p-2"
+      >
+        {Array.from({ length: 20 }).map((_, i) => (
+          <div key={i}>
+            Query {query} row {i}
+          </div>
+        ))}
+      </div>
+    </div>
+  )
+}
diff --git a/e2e/react-start/scroll-restoration/src/routes/index.tsx b/e2e/react-start/scroll-restoration/src/routes/index.tsx
index 9d36cca264..ce0e57417c 100644
--- a/e2e/react-start/scroll-restoration/src/routes/index.tsx
+++ b/e2e/react-start/scroll-restoration/src/routes/index.tsx
@@ -30,6 +30,20 @@ function HomeComponent() {
           </p>
         </div>
       ))}
+      <div className="border p-2">
+        <h4>scroll restoration repro routes</h4>
+        <p>
+          <Link to="/hash-scroll-repro">/hash-scroll-repro</Link>
+        </p>
+        <p>
+          <Link to="/nested-scroll-carry-over-a">
+            /nested-scroll-carry-over-a
+          </Link>
+        </p>
+        <p>
+          <Link to="/nested-scroll-search">/nested-scroll-search</Link>
+        </p>
+      </div>
     </div>
   )
 }
diff --git a/e2e/react-start/scroll-restoration/tests/nested-scroll-carry-over.spec.ts b/e2e/react-start/scroll-restoration/tests/nested-scroll-carry-over.spec.ts
new file mode 100644
index 0000000000..b26a6e32d3
--- /dev/null
+++ b/e2e/react-start/scroll-restoration/tests/nested-scroll-carry-over.spec.ts
@@ -0,0 +1,196 @@
+import { expect, test } from '@playwright/test'
+
+const storageKey = 'tsr-scroll-restoration-v1_3'
+
+test('carries existing nested scroll entries to the next key', async ({
+  page,
+}) => {
+  await page.goto('/nested-scroll-carry-over-a')
+  await page.waitForLoadState('networkidle')
+  await expect(
+    page.getByRole('heading', { name: 'nested-scroll-carry-over-a' }),
+  ).toBeVisible()
+
+  const positions = await page.evaluate(() => {
+    const getScroller = (testId: string) => {
+      const element = document.querySelector(`[data-testid="${testId}"]`)
+      if (!(element instanceof HTMLElement)) {
+        throw new Error(`Missing ${testId}`)
+      }
+      return element
+    }
+
+    const preserved = getScroller('carry-over-preserved')
+    const reset = getScroller('carry-over-reset')
+    const sourceOnly = getScroller('carry-over-source-only')
+
+    preserved.scrollTop = 80
+    reset.scrollTop = 80
+    sourceOnly.scrollTop = 80
+
+    preserved.dispatchEvent(new Event('scroll', { bubbles: true }))
+    reset.dispatchEvent(new Event('scroll', { bubbles: true }))
+    sourceOnly.dispatchEvent(new Event('scroll', { bubbles: true }))
+
+    return {
+      preserved: preserved.scrollTop,
+      reset: reset.scrollTop,
+      sourceOnly: sourceOnly.scrollTop,
+    }
+  })
+
+  expect(positions.preserved).toBeGreaterThan(0)
+  expect(positions.reset).toBeGreaterThan(0)
+  expect(positions.sourceOnly).toBeGreaterThan(0)
+
+  await page.getByTestId('nested-scroll-carry-over-link').click()
+  await expect(
+    page.getByRole('heading', { name: 'nested-scroll-carry-over-b' }),
+  ).toBeVisible()
+
+  await expect
+    .poll(async () => {
+      return page.evaluate(() => {
+        const element = document.querySelector(
+          '[data-testid="carry-over-preserved"]',
+        )
+        return element instanceof HTMLElement ? element.scrollTop : 0
+      })
+    })
+    .toBe(positions.preserved)
+
+  await expect
+    .poll(async () => {
+      return page.evaluate(() => {
+        const element = document.querySelector(
+          '[data-testid="carry-over-reset"]',
+        )
+        return element instanceof HTMLElement ? element.scrollTop : -1
+      })
+    })
+    .toBe(0)
+
+  await page.reload()
+  await page.waitForLoadState('networkidle')
+  await expect(
+    page.getByRole('heading', { name: 'nested-scroll-carry-over-b' }),
+  ).toBeVisible()
+
+  await expect
+    .poll(async () => {
+      return page.evaluate(() => {
+        const element = document.querySelector(
+          '[data-testid="carry-over-preserved"]',
+        )
+        return element instanceof HTMLElement ? element.scrollTop : 0
+      })
+    })
+    .toBe(positions.preserved)
+
+  await expect
+    .poll(async () => {
+      return page.evaluate(() => {
+        const element = document.querySelector(
+          '[data-testid="carry-over-reset"]',
+        )
+        return element instanceof HTMLElement ? element.scrollTop : -1
+      })
+    })
+    .toBe(0)
+
+  const targetKeyEntries = await page.evaluate((scrollStorageKey) => {
+    const targetKey = window.history.state.__TSR_key
+    const raw = window.sessionStorage.getItem(scrollStorageKey)
+    const state = raw ? JSON.parse(raw) : {}
+    const entries = state[targetKey] ?? {}
+
+    return {
+      hasPreserved: Object.hasOwn(
+        entries,
+        '[data-scroll-restoration-id="carry-over-preserved"]',
+      ),
+      hasReset: Object.hasOwn(
+        entries,
+        '[data-scroll-restoration-id="carry-over-reset"]',
+      ),
+      hasSourceOnly: Object.hasOwn(
+        entries,
+        '[data-scroll-restoration-id="carry-over-source-only"]',
+      ),
+    }
+  }, storageKey)
+
+  expect(targetKeyEntries).toEqual({
+    hasPreserved: true,
+    hasReset: false,
+    hasSourceOnly: false,
+  })
+})
+
+test('restores carried nested scroll after search navigation and browser back', async ({
+  page,
+}) => {
+  await page.goto('/nested-scroll-search')
+  await page.waitForLoadState('networkidle')
+  await expect(
+    page.getByRole('heading', { name: 'nested-scroll-search' }),
+  ).toBeVisible()
+
+  const scrollTop = await page.evaluate(() => {
+    const element = document.querySelector(
+      '[data-testid="nested-scroll-search-container"]',
+    )
+    if (!(element instanceof HTMLElement)) {
+      throw new Error('Missing nested scroll container')
+    }
+
+    element.scrollTop = 80
+    element.dispatchEvent(new Event('scroll', { bubbles: true }))
+    return element.scrollTop
+  })
+
+  expect(scrollTop).toBeGreaterThan(0)
+
+  await page.getByTestId('nested-scroll-search-link').click()
+  await expect(page.getByTestId('nested-scroll-search-query')).toHaveText(
+    'query: xyz',
+  )
+
+  await expect
+    .poll(async () => {
+      return page.evaluate(() => {
+        const element = document.querySelector(
+          '[data-testid="nested-scroll-search-container"]',
+        )
+        return element instanceof HTMLElement ? element.scrollTop : 0
+      })
+    })
+    .toBe(scrollTop)
+
+  await page.getByTestId('nested-scroll-away-link').click()
+  await expect(
+    page.getByRole('heading', { name: 'nested-scroll-away' }),
+  ).toBeVisible()
+  await expect(page.getByTestId('nested-scroll-search-container')).toHaveCount(
+    0,
+  )
+
+  await page.goBack()
+  await expect(
+    page.getByRole('heading', { name: 'nested-scroll-search' }),
+  ).toBeVisible()
+  await expect(page.getByTestId('nested-scroll-search-query')).toHaveText(
+    'query: xyz',
+  )
+
+  await expect
+    .poll(async () => {
+      return page.evaluate(() => {
+        const element = document.querySelector(
+          '[data-testid="nested-scroll-search-container"]',
+        )
+        return element instanceof HTMLElement ? element.scrollTop : 0
+      })
+    })
+    .toBe(scrollTop)
+})
diff --git a/packages/router-core/src/scroll-restoration.ts b/packages/router-core/src/scroll-restoration.ts
index d4ec57fbdb..83966b0e9a 100644
--- a/packages/router-core/src/scroll-restoration.ts
+++ b/packages/router-core/src/scroll-restoration.ts
@@ -1,9 +1,8 @@
 import { isServer } from '@tanstack/router-core/isServer'
-import { functionalUpdate, isPlainObject } from './utils'
+import { isPlainObject } from './utils'
 import { historyActionKey } from './router'
 import type { AnyRouter, ParsedLocationWithHistoryAction } from './router'
 import type { ParsedLocation } from './location'
-import type { NonNullableUpdater } from './utils'
 
 export type ScrollRestorationEntry = { scrollX: number; scrollY: number }
 
@@ -13,7 +12,6 @@ type ScrollRestorationByKey = Record<string, ScrollRestorationByElement>
 
 type ScrollRestorationCache = {
   readonly state: ScrollRestorationByKey
-  set: (updater: NonNullableUpdater<ScrollRestorationByKey>) => void
   persist: () => void
 }
 
@@ -70,9 +68,6 @@ function createScrollRestorationCache(): ScrollRestorationCache | null {
     get state() {
       return state
     },
-    set: (updater) => {
-      state = functionalUpdate(updater, state) || state
-    },
     persist,
   }
 }
@@ -89,15 +84,23 @@ export const defaultGetScrollRestorationKey = (location: ParsedLocation) => {
   return location.state.__TSR_key! || location.href
 }
 
-function getCssSelector(el: any): string {
+function getScrollRestorationSelector(element: Element): string {
+  const attrId = element.getAttribute(scrollRestorationIdAttribute)
+  if (attrId) {
+    return `[${scrollRestorationIdAttribute}="${attrId}"]`
+  }
+
   const path = []
+  let el: any = element
   let parent: HTMLElement
+
   while ((parent = el.parentNode)) {
     path.push(
       `${el.tagName}:nth-child(${Array.prototype.indexOf.call(parent.children, el) + 1})`,
     )
     el = parent
   }
+
   return `${path.reverse().join(' > ')}`.toLowerCase()
 }
 
@@ -131,7 +134,9 @@ export function getElementScrollRestorationEntry(
   }
 
   return scrollRestorationCache?.state[restoreKey]?.[
-    element instanceof Window ? windowScrollTarget : getCssSelector(element)
+    element instanceof Window
+      ? windowScrollTarget
+      : getScrollRestorationSelector(element)
   ]
 }
 
@@ -140,13 +145,40 @@ const windowScrollTarget = 'window'
 const scrollRestorationIdAttribute = 'data-scroll-restoration-id'
 type ScrollTarget = typeof windowScrollTarget | Element
 
+function getElement(selector: string | (() => Element | null | undefined)) {
+  try {
+    return typeof selector === 'function'
+      ? selector()
+      : document.querySelector(selector)
+  } catch {}
+  return
+}
+
+function getScrollToTopElements(
+  scrollToTopSelectors: NonNullable<
+    AnyRouter['options']['scrollToTopSelectors']
+  >,
+): Array<Element> {
+  const elements: Array<Element> = []
+
+  for (const selector of scrollToTopSelectors) {
+    if (selector === windowScrollTarget) {
+      continue
+    }
+
+    const element = getElement(selector)
+    if (element) {
+      elements.push(element)
+    }
+  }
+
+  return elements
+}
+
 export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
   // Keep hash/top scrolling active even when sessionStorage is unavailable.
 
-  const shouldScrollRestoration =
-    force ?? router.options.scrollRestoration ?? false
-
-  if (shouldScrollRestoration) {
+  if (force ?? router.options.scrollRestoration ?? false) {
     router.isScrollRestoring = true
   }
 
@@ -197,22 +229,11 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
       {} as ScrollRestorationByElement)
 
     for (const [target, position] of trackedScrollEntries) {
-      let selector: string | undefined
-
       if (target === windowScrollTarget) {
-        selector = windowScrollTarget
+        keyEntry[windowScrollTarget] = position
       } else if (target.isConnected) {
-        const attrId = target.getAttribute(scrollRestorationIdAttribute)
-        selector = attrId
-          ? `[${scrollRestorationIdAttribute}="${attrId}"]`
-          : getCssSelector(target)
-      }
-
-      if (!selector) {
-        continue
+        keyEntry[getScrollRestorationSelector(target)] = position
       }
-
-      keyEntry[selector] = position
     }
   }
 
@@ -234,7 +255,6 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
 
   // Restore destination scroll after the new route has rendered.
   router.subscribe('onRendered', (event) => {
-    const cacheKey = getKey(event.toLocation)
     const behavior = router.options.scrollRestorationBehavior
     const scrollToTopSelectors = router.options.scrollToTopSelectors
     trackedScrollEntries.clear()
@@ -251,6 +271,9 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
       return
     }
 
+    const cacheKey = getKey(event.toLocation)
+    const fromCacheKey = event.fromLocation && getKey(event.fromLocation)
+
     ignoreScroll = true
 
     try {
@@ -264,6 +287,48 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
         hash &&
         hashScrollIntoViewOptions &&
         (action === 'PUSH' || action === 'REPLACE')
+      let scrollToTopElements: Array<Element> | undefined
+
+      if (
+        router.isScrollRestoring &&
+        scrollRestorationCache &&
+        fromCacheKey &&
+        fromCacheKey !== cacheKey
+      ) {
+        const fromElementEntries = scrollRestorationCache.state[fromCacheKey]
+
+        if (fromElementEntries) {
+          let toElementEntries = scrollRestorationCache.state[cacheKey]
+
+          if (scrollToTopSelectors) {
+            scrollToTopElements = getScrollToTopElements(scrollToTopSelectors)
+          }
+
+          for (const elementSelector in fromElementEntries) {
+            if (elementSelector === windowScrollTarget) {
+              continue
+            }
+
+            const element = getElement(elementSelector)
+            if (!element) {
+              continue
+            }
+
+            if (scrollToTopElements?.includes(element)) {
+              continue
+            }
+
+            if (!toElementEntries) {
+              toElementEntries = scrollRestorationCache.state[cacheKey] =
+                {} as ScrollRestorationByElement
+            }
+
+            toElementEntries[elementSelector] ??=
+              fromElementEntries[elementSelector]!
+          }
+        }
+      }
+
       const elementEntries = router.isScrollRestoring
         ? scrollRestorationCache?.state[cacheKey]
         : undefined
@@ -297,15 +362,8 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
               behavior,
             })
             windowRestored = true
-          } else if (elementSelector) {
-            let element
-
-            try {
-              element = document.querySelector(elementSelector)
-            } catch {
-              continue
-            }
-
+          } else {
+            const element = getElement(elementSelector)
             if (element) {
               element.scrollLeft = scrollX as number
               element.scrollTop = scrollY as number
@@ -330,15 +388,9 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
 
           window.scrollTo(scrollOptions)
           if (scrollToTopSelectors) {
-            for (const selector of scrollToTopSelectors) {
-              if (selector === windowScrollTarget) continue
-              const element =
-                typeof selector === 'function'
-                  ? selector()
-                  : document.querySelector(selector)
-              if (element) {
-                element.scrollTo(scrollOptions)
-              }
+            scrollToTopElements ??= getScrollToTopElements(scrollToTopSelectors)
+            for (const element of scrollToTopElements) {
+              element.scrollTo(scrollOptions)
             }
           }
         }
@@ -348,10 +400,8 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
     }
 
     if (router.isScrollRestoring && scrollRestorationCache) {
-      scrollRestorationCache.set((state) => {
-        state[cacheKey] ||= {} as ScrollRestorationByElement
-        return state
-      })
+      scrollRestorationCache.state[cacheKey] ||=
+        {} as ScrollRestorationByElement
     }
   })
 }

From 34e18a29912d1cd989a9f50ea511b8a0a48d6014 Mon Sep 17 00:00:00 2001
From: Manuel Schiller <manuel.schiller@caligano.de>
Date: Wed, 20 May 2026 22:09:50 +0200
Subject: [PATCH 4/6] fix: preserve carried scroll across resetScroll=false
 navigations

---
 .changeset/soft-badgers-sneeze.md             |  2 +-
 .../scroll-restoration/src/routeTree.gen.ts   | 63 +++++++++++++
 .../routes/(tests)/reset-scroll-false-a.tsx   | 15 +++
 .../routes/(tests)/reset-scroll-false-b.tsx   | 15 +++
 .../routes/(tests)/reset-scroll-false-c.tsx   | 15 +++
 .../-components/reset-scroll-false-list.tsx   | 23 +++++
 .../scroll-restoration/src/routes/index.tsx   |  3 +
 .../tests/reset-scroll-false.spec.ts          | 52 +++++++++++
 packages/router-core/src/router.ts            | 50 +++++-----
 .../router-core/src/scroll-restoration.ts     | 92 ++++++++++---------
 10 files changed, 261 insertions(+), 69 deletions(-)
 create mode 100644 e2e/react-start/scroll-restoration/src/routes/(tests)/reset-scroll-false-a.tsx
 create mode 100644 e2e/react-start/scroll-restoration/src/routes/(tests)/reset-scroll-false-b.tsx
 create mode 100644 e2e/react-start/scroll-restoration/src/routes/(tests)/reset-scroll-false-c.tsx
 create mode 100644 e2e/react-start/scroll-restoration/src/routes/-components/reset-scroll-false-list.tsx
 create mode 100644 e2e/react-start/scroll-restoration/tests/reset-scroll-false.spec.ts

diff --git a/.changeset/soft-badgers-sneeze.md b/.changeset/soft-badgers-sneeze.md
index cb97946ee9..2ce7333c61 100644
--- a/.changeset/soft-badgers-sneeze.md
+++ b/.changeset/soft-badgers-sneeze.md
@@ -2,4 +2,4 @@
 '@tanstack/router-core': patch
 ---
 
-Preserve existing nested scroll positions across SPA navigations that create new restoration keys.
+Preserve carried scroll positions across SPA navigations that create new restoration keys.
diff --git a/e2e/react-start/scroll-restoration/src/routeTree.gen.ts b/e2e/react-start/scroll-restoration/src/routeTree.gen.ts
index 07cb84ed0b..5c9174efcc 100644
--- a/e2e/react-start/scroll-restoration/src/routeTree.gen.ts
+++ b/e2e/react-start/scroll-restoration/src/routeTree.gen.ts
@@ -12,6 +12,9 @@ import { Route as rootRouteImport } from './routes/__root'
 import { Route as IndexRouteImport } from './routes/index'
 import { Route as testsWithSearchRouteImport } from './routes/(tests)/with-search'
 import { Route as testsWithLoaderRouteImport } from './routes/(tests)/with-loader'
+import { Route as testsResetScrollFalseCRouteImport } from './routes/(tests)/reset-scroll-false-c'
+import { Route as testsResetScrollFalseBRouteImport } from './routes/(tests)/reset-scroll-false-b'
+import { Route as testsResetScrollFalseARouteImport } from './routes/(tests)/reset-scroll-false-a'
 import { Route as testsNormalPageRouteImport } from './routes/(tests)/normal-page'
 import { Route as testsNestedScrollSearchRouteImport } from './routes/(tests)/nested-scroll-search'
 import { Route as testsNestedScrollCarryOverBRouteImport } from './routes/(tests)/nested-scroll-carry-over-b'
@@ -35,6 +38,21 @@ const testsWithLoaderRoute = testsWithLoaderRouteImport.update({
   path: '/with-loader',
   getParentRoute: () => rootRouteImport,
 } as any)
+const testsResetScrollFalseCRoute = testsResetScrollFalseCRouteImport.update({
+  id: '/(tests)/reset-scroll-false-c',
+  path: '/reset-scroll-false-c',
+  getParentRoute: () => rootRouteImport,
+} as any)
+const testsResetScrollFalseBRoute = testsResetScrollFalseBRouteImport.update({
+  id: '/(tests)/reset-scroll-false-b',
+  path: '/reset-scroll-false-b',
+  getParentRoute: () => rootRouteImport,
+} as any)
+const testsResetScrollFalseARoute = testsResetScrollFalseARouteImport.update({
+  id: '/(tests)/reset-scroll-false-a',
+  path: '/reset-scroll-false-a',
+  getParentRoute: () => rootRouteImport,
+} as any)
 const testsNormalPageRoute = testsNormalPageRouteImport.update({
   id: '/(tests)/normal-page',
   path: '/normal-page',
@@ -82,6 +100,9 @@ export interface FileRoutesByFullPath {
   '/nested-scroll-carry-over-b': typeof testsNestedScrollCarryOverBRoute
   '/nested-scroll-search': typeof testsNestedScrollSearchRoute
   '/normal-page': typeof testsNormalPageRoute
+  '/reset-scroll-false-a': typeof testsResetScrollFalseARoute
+  '/reset-scroll-false-b': typeof testsResetScrollFalseBRoute
+  '/reset-scroll-false-c': typeof testsResetScrollFalseCRoute
   '/with-loader': typeof testsWithLoaderRoute
   '/with-search': typeof testsWithSearchRoute
 }
@@ -94,6 +115,9 @@ export interface FileRoutesByTo {
   '/nested-scroll-carry-over-b': typeof testsNestedScrollCarryOverBRoute
   '/nested-scroll-search': typeof testsNestedScrollSearchRoute
   '/normal-page': typeof testsNormalPageRoute
+  '/reset-scroll-false-a': typeof testsResetScrollFalseARoute
+  '/reset-scroll-false-b': typeof testsResetScrollFalseBRoute
+  '/reset-scroll-false-c': typeof testsResetScrollFalseCRoute
   '/with-loader': typeof testsWithLoaderRoute
   '/with-search': typeof testsWithSearchRoute
 }
@@ -107,6 +131,9 @@ export interface FileRoutesById {
   '/(tests)/nested-scroll-carry-over-b': typeof testsNestedScrollCarryOverBRoute
   '/(tests)/nested-scroll-search': typeof testsNestedScrollSearchRoute
   '/(tests)/normal-page': typeof testsNormalPageRoute
+  '/(tests)/reset-scroll-false-a': typeof testsResetScrollFalseARoute
+  '/(tests)/reset-scroll-false-b': typeof testsResetScrollFalseBRoute
+  '/(tests)/reset-scroll-false-c': typeof testsResetScrollFalseCRoute
   '/(tests)/with-loader': typeof testsWithLoaderRoute
   '/(tests)/with-search': typeof testsWithSearchRoute
 }
@@ -121,6 +148,9 @@ export interface FileRouteTypes {
     | '/nested-scroll-carry-over-b'
     | '/nested-scroll-search'
     | '/normal-page'
+    | '/reset-scroll-false-a'
+    | '/reset-scroll-false-b'
+    | '/reset-scroll-false-c'
     | '/with-loader'
     | '/with-search'
   fileRoutesByTo: FileRoutesByTo
@@ -133,6 +163,9 @@ export interface FileRouteTypes {
     | '/nested-scroll-carry-over-b'
     | '/nested-scroll-search'
     | '/normal-page'
+    | '/reset-scroll-false-a'
+    | '/reset-scroll-false-b'
+    | '/reset-scroll-false-c'
     | '/with-loader'
     | '/with-search'
   id:
@@ -145,6 +178,9 @@ export interface FileRouteTypes {
     | '/(tests)/nested-scroll-carry-over-b'
     | '/(tests)/nested-scroll-search'
     | '/(tests)/normal-page'
+    | '/(tests)/reset-scroll-false-a'
+    | '/(tests)/reset-scroll-false-b'
+    | '/(tests)/reset-scroll-false-c'
     | '/(tests)/with-loader'
     | '/(tests)/with-search'
   fileRoutesById: FileRoutesById
@@ -158,6 +194,9 @@ export interface RootRouteChildren {
   testsNestedScrollCarryOverBRoute: typeof testsNestedScrollCarryOverBRoute
   testsNestedScrollSearchRoute: typeof testsNestedScrollSearchRoute
   testsNormalPageRoute: typeof testsNormalPageRoute
+  testsResetScrollFalseARoute: typeof testsResetScrollFalseARoute
+  testsResetScrollFalseBRoute: typeof testsResetScrollFalseBRoute
+  testsResetScrollFalseCRoute: typeof testsResetScrollFalseCRoute
   testsWithLoaderRoute: typeof testsWithLoaderRoute
   testsWithSearchRoute: typeof testsWithSearchRoute
 }
@@ -185,6 +224,27 @@ declare module '@tanstack/react-router' {
       preLoaderRoute: typeof testsWithLoaderRouteImport
       parentRoute: typeof rootRouteImport
     }
+    '/(tests)/reset-scroll-false-c': {
+      id: '/(tests)/reset-scroll-false-c'
+      path: '/reset-scroll-false-c'
+      fullPath: '/reset-scroll-false-c'
+      preLoaderRoute: typeof testsResetScrollFalseCRouteImport
+      parentRoute: typeof rootRouteImport
+    }
+    '/(tests)/reset-scroll-false-b': {
+      id: '/(tests)/reset-scroll-false-b'
+      path: '/reset-scroll-false-b'
+      fullPath: '/reset-scroll-false-b'
+      preLoaderRoute: typeof testsResetScrollFalseBRouteImport
+      parentRoute: typeof rootRouteImport
+    }
+    '/(tests)/reset-scroll-false-a': {
+      id: '/(tests)/reset-scroll-false-a'
+      path: '/reset-scroll-false-a'
+      fullPath: '/reset-scroll-false-a'
+      preLoaderRoute: typeof testsResetScrollFalseARouteImport
+      parentRoute: typeof rootRouteImport
+    }
     '/(tests)/normal-page': {
       id: '/(tests)/normal-page'
       path: '/normal-page'
@@ -246,6 +306,9 @@ const rootRouteChildren: RootRouteChildren = {
   testsNestedScrollCarryOverBRoute: testsNestedScrollCarryOverBRoute,
   testsNestedScrollSearchRoute: testsNestedScrollSearchRoute,
   testsNormalPageRoute: testsNormalPageRoute,
+  testsResetScrollFalseARoute: testsResetScrollFalseARoute,
+  testsResetScrollFalseBRoute: testsResetScrollFalseBRoute,
+  testsResetScrollFalseCRoute: testsResetScrollFalseCRoute,
   testsWithLoaderRoute: testsWithLoaderRoute,
   testsWithSearchRoute: testsWithSearchRoute,
 }
diff --git a/e2e/react-start/scroll-restoration/src/routes/(tests)/reset-scroll-false-a.tsx b/e2e/react-start/scroll-restoration/src/routes/(tests)/reset-scroll-false-a.tsx
new file mode 100644
index 0000000000..89103f1194
--- /dev/null
+++ b/e2e/react-start/scroll-restoration/src/routes/(tests)/reset-scroll-false-a.tsx
@@ -0,0 +1,15 @@
+import { createFileRoute } from '@tanstack/react-router'
+import { ResetScrollFalseList } from '../-components/reset-scroll-false-list'
+
+export const Route = createFileRoute('/(tests)/reset-scroll-false-a')({
+  component: Component,
+})
+
+function Component() {
+  return (
+    <div className="p-2">
+      <h3>reset-scroll-false-a</h3>
+      <ResetScrollFalseList to="/reset-scroll-false-b" />
+    </div>
+  )
+}
diff --git a/e2e/react-start/scroll-restoration/src/routes/(tests)/reset-scroll-false-b.tsx b/e2e/react-start/scroll-restoration/src/routes/(tests)/reset-scroll-false-b.tsx
new file mode 100644
index 0000000000..19713d91e6
--- /dev/null
+++ b/e2e/react-start/scroll-restoration/src/routes/(tests)/reset-scroll-false-b.tsx
@@ -0,0 +1,15 @@
+import { createFileRoute } from '@tanstack/react-router'
+import { ResetScrollFalseList } from '../-components/reset-scroll-false-list'
+
+export const Route = createFileRoute('/(tests)/reset-scroll-false-b')({
+  component: Component,
+})
+
+function Component() {
+  return (
+    <div className="p-2">
+      <h3>reset-scroll-false-b</h3>
+      <ResetScrollFalseList to="/reset-scroll-false-c" />
+    </div>
+  )
+}
diff --git a/e2e/react-start/scroll-restoration/src/routes/(tests)/reset-scroll-false-c.tsx b/e2e/react-start/scroll-restoration/src/routes/(tests)/reset-scroll-false-c.tsx
new file mode 100644
index 0000000000..b7c2a853f6
--- /dev/null
+++ b/e2e/react-start/scroll-restoration/src/routes/(tests)/reset-scroll-false-c.tsx
@@ -0,0 +1,15 @@
+import { createFileRoute } from '@tanstack/react-router'
+import { ResetScrollFalseList } from '../-components/reset-scroll-false-list'
+
+export const Route = createFileRoute('/(tests)/reset-scroll-false-c')({
+  component: Component,
+})
+
+function Component() {
+  return (
+    <div className="p-2">
+      <h3>reset-scroll-false-c</h3>
+      <ResetScrollFalseList />
+    </div>
+  )
+}
diff --git a/e2e/react-start/scroll-restoration/src/routes/-components/reset-scroll-false-list.tsx b/e2e/react-start/scroll-restoration/src/routes/-components/reset-scroll-false-list.tsx
new file mode 100644
index 0000000000..cdfe934aa1
--- /dev/null
+++ b/e2e/react-start/scroll-restoration/src/routes/-components/reset-scroll-false-list.tsx
@@ -0,0 +1,23 @@
+import { Link } from '@tanstack/react-router'
+import type { CSSProperties } from 'react'
+import type { LinkOptions } from '@tanstack/react-router'
+
+const style = { height: '100px', width: '50%' } satisfies CSSProperties
+
+export function ResetScrollFalseList({ to }: { to?: LinkOptions['to'] }) {
+  return Array.from({ length: 100 }, (_, i) => {
+    const label = i + 1
+
+    return (
+      <div key={label} style={style}>
+        <Link
+          to={to}
+          resetScroll={false}
+          data-testid={`reset-scroll-false-link-${label}`}
+        >
+          {label}
+        </Link>
+      </div>
+    )
+  })
+}
diff --git a/e2e/react-start/scroll-restoration/src/routes/index.tsx b/e2e/react-start/scroll-restoration/src/routes/index.tsx
index ce0e57417c..81835a5582 100644
--- a/e2e/react-start/scroll-restoration/src/routes/index.tsx
+++ b/e2e/react-start/scroll-restoration/src/routes/index.tsx
@@ -43,6 +43,9 @@ function HomeComponent() {
         <p>
           <Link to="/nested-scroll-search">/nested-scroll-search</Link>
         </p>
+        <p>
+          <Link to="/reset-scroll-false-a">/reset-scroll-false-a</Link>
+        </p>
       </div>
     </div>
   )
diff --git a/e2e/react-start/scroll-restoration/tests/reset-scroll-false.spec.ts b/e2e/react-start/scroll-restoration/tests/reset-scroll-false.spec.ts
new file mode 100644
index 0000000000..c153a42d19
--- /dev/null
+++ b/e2e/react-start/scroll-restoration/tests/reset-scroll-false.spec.ts
@@ -0,0 +1,52 @@
+import { expect, test } from '@playwright/test'
+
+test('preserves carried window scroll for intermediate resetScroll=false entries', async ({
+  page,
+}) => {
+  await page.goto('/reset-scroll-false-a')
+  await page.waitForLoadState('networkidle')
+  await expect(
+    page.getByRole('heading', { name: 'reset-scroll-false-a' }),
+  ).toBeVisible()
+
+  const scrollY = await page.evaluate(async () => {
+    window.scrollTo(0, 500)
+    window.dispatchEvent(new Event('scroll'))
+    await new Promise((resolve) => requestAnimationFrame(() => resolve(null)))
+    return window.scrollY
+  })
+
+  expect(scrollY).toBeGreaterThan(0)
+  await expect(page.getByTestId('reset-scroll-false-link-8')).toBeInViewport()
+
+  await page.getByTestId('reset-scroll-false-link-8').click()
+  await expect(page).toHaveURL(/\/reset-scroll-false-b$/)
+  await expect(
+    page.getByRole('heading', { name: 'reset-scroll-false-b' }),
+  ).toHaveCount(1)
+
+  await expect
+    .poll(async () => page.evaluate(() => window.scrollY))
+    .toBe(scrollY)
+  await expect(page.getByTestId('reset-scroll-false-link-8')).toBeInViewport()
+
+  await page.getByTestId('reset-scroll-false-link-8').click()
+  await expect(page).toHaveURL(/\/reset-scroll-false-c$/)
+  await expect(
+    page.getByRole('heading', { name: 'reset-scroll-false-c' }),
+  ).toHaveCount(1)
+
+  await expect
+    .poll(async () => page.evaluate(() => window.scrollY))
+    .toBe(scrollY)
+
+  await page.goBack()
+  await expect(page).toHaveURL(/\/reset-scroll-false-b$/)
+  await expect(
+    page.getByRole('heading', { name: 'reset-scroll-false-b' }),
+  ).toHaveCount(1)
+
+  await expect
+    .poll(async () => page.evaluate(() => window.scrollY))
+    .toBe(scrollY)
+})
diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts
index 83d23eddbf..8ce3866c6c 100644
--- a/packages/router-core/src/router.ts
+++ b/packages/router-core/src/router.ts
@@ -887,18 +887,21 @@ export function getLocationChangeInfo(
   return { fromLocation, toLocation, pathChanged, hrefChanged, hashChanged }
 }
 
-/**
- * Symbol-keyed slot on ParsedLocation carrying the HistoryAction that
- * triggered the current load. Read by the scroll-restoration listener to
- * decide whether to skip stale window scroll restoration in favor of hash
- * navigation. Symbol keys are excluded from Object.keys/JSON/spread, so this
- * is invisible to user code.
- * @private
- */
-export const historyActionKey: unique symbol = Symbol()
+const locationHistoryActions = new WeakMap<ParsedLocation, HistoryAction>()
+
+export function getLocationHistoryAction(location: ParsedLocation) {
+  return locationHistoryActions.get(location)
+}
 
-export type ParsedLocationWithHistoryAction = ParsedLocation & {
-  [historyActionKey]?: HistoryAction
+function setLocationHistoryAction(
+  location: ParsedLocation,
+  action: HistoryAction | undefined,
+) {
+  if (action) {
+    locationHistoryActions.set(location, action)
+  } else {
+    locationHistoryActions.delete(location)
+  }
 }
 
 export type CreateRouterFn = <
@@ -2134,6 +2137,7 @@ export class RouterCore<
     ignoreBlocker,
     ...next
   }) => {
+    let historyAction: HistoryAction | undefined
     const isSameState = () => {
       // the following props are ignored but may still be provided when navigating,
       // temporarily add the previous values to the next state so they don't affect
@@ -2209,7 +2213,9 @@ export class RouterCore<
 
       this.shouldViewTransition = viewTransition
 
-      this.history[next.replace ? 'replace' : 'push'](
+      historyAction = next.replace ? 'REPLACE' : 'PUSH'
+
+      this.history[historyAction === 'REPLACE' ? 'replace' : 'push'](
         nextHistory.publicHref,
         nextHistory.state,
         { ignoreBlocker },
@@ -2219,7 +2225,13 @@ export class RouterCore<
     this.resetNextScroll = next.resetScroll ?? true
 
     if (!this.history.subscribers.size) {
-      this.load()
+      this.load(
+        historyAction
+          ? {
+              action: { type: historyAction },
+            }
+          : undefined,
+      )
     }
 
     return this.commitLocationPromise
@@ -2434,17 +2446,7 @@ export class RouterCore<
       this.startTransition(async () => {
         try {
           this.beforeLoad()
-          // Stamp action onto the location instance via symbol key so downstream
-          // emitters of locationChangeInfo (e.g. onRendered from Match) can read
-          // it. Only set when an action is present; no-action loads (invalidate,
-          // same-URL commit, SSR hydration) leave the key absent so that
-          // deep-equality checks (e.g. toEqual) on location objects are not
-          // affected by an extraneous Symbol → undefined entry.
-          if (historyAction !== undefined) {
-            ;(this.latestLocation as ParsedLocationWithHistoryAction)[
-              historyActionKey
-            ] = historyAction
-          }
+          setLocationHistoryAction(this.latestLocation, historyAction)
           const next = this.latestLocation
           const prevLocation = this.stores.resolvedLocation.get()
           const locationChangeInfo = getLocationChangeInfo(next, prevLocation)
diff --git a/packages/router-core/src/scroll-restoration.ts b/packages/router-core/src/scroll-restoration.ts
index 83966b0e9a..f2377f8c1b 100644
--- a/packages/router-core/src/scroll-restoration.ts
+++ b/packages/router-core/src/scroll-restoration.ts
@@ -1,7 +1,7 @@
 import { isServer } from '@tanstack/router-core/isServer'
 import { isPlainObject } from './utils'
-import { historyActionKey } from './router'
-import type { AnyRouter, ParsedLocationWithHistoryAction } from './router'
+import { getLocationHistoryAction } from './router'
+import type { AnyRouter } from './router'
 import type { ParsedLocation } from './location'
 
 export type ScrollRestorationEntry = { scrollX: number; scrollY: number }
@@ -257,11 +257,12 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
   router.subscribe('onRendered', (event) => {
     const behavior = router.options.scrollRestorationBehavior
     const scrollToTopSelectors = router.options.scrollToTopSelectors
+    const shouldResetScroll = router.resetNextScroll
+    let scrollToTopElements: Array<Element> | undefined
     trackedScrollEntries.clear()
 
-    if (!router.resetNextScroll) {
+    if (!shouldResetScroll) {
       router.resetNextScroll = true
-      return
     }
 
     if (
@@ -274,60 +275,63 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
     const cacheKey = getKey(event.toLocation)
     const fromCacheKey = event.fromLocation && getKey(event.fromLocation)
 
-    ignoreScroll = true
-
-    try {
-      const hash = event.toLocation.hash
-      const hashScrollIntoViewOptions =
-        event.toLocation.state.__hashScrollIntoViewOptions ?? true
-      const action = (event.toLocation as ParsedLocationWithHistoryAction)[
-        historyActionKey
-      ]
-      const skipWindowRestore =
-        hash &&
-        hashScrollIntoViewOptions &&
-        (action === 'PUSH' || action === 'REPLACE')
-      let scrollToTopElements: Array<Element> | undefined
-
-      if (
-        router.isScrollRestoring &&
-        scrollRestorationCache &&
-        fromCacheKey &&
-        fromCacheKey !== cacheKey
-      ) {
-        const fromElementEntries = scrollRestorationCache.state[fromCacheKey]
-
-        if (fromElementEntries) {
-          let toElementEntries = scrollRestorationCache.state[cacheKey]
+    if (
+      router.isScrollRestoring &&
+      scrollRestorationCache &&
+      fromCacheKey &&
+      fromCacheKey !== cacheKey
+    ) {
+      const fromElementEntries = scrollRestorationCache.state[fromCacheKey]
 
-          if (scrollToTopSelectors) {
-            scrollToTopElements = getScrollToTopElements(scrollToTopSelectors)
-          }
+      if (fromElementEntries) {
+        let toElementEntries = scrollRestorationCache.state[cacheKey]
 
-          for (const elementSelector in fromElementEntries) {
-            if (elementSelector === windowScrollTarget) {
+        for (const elementSelector in fromElementEntries) {
+          if (elementSelector === windowScrollTarget) {
+            if (shouldResetScroll) {
               continue
             }
-
+          } else {
             const element = getElement(elementSelector)
             if (!element) {
               continue
             }
 
-            if (scrollToTopElements?.includes(element)) {
-              continue
-            }
-
-            if (!toElementEntries) {
-              toElementEntries = scrollRestorationCache.state[cacheKey] =
-                {} as ScrollRestorationByElement
+            if (shouldResetScroll && scrollToTopSelectors) {
+              scrollToTopElements ??=
+                getScrollToTopElements(scrollToTopSelectors)
+              if (scrollToTopElements.includes(element)) {
+                continue
+              }
             }
+          }
 
-            toElementEntries[elementSelector] ??=
-              fromElementEntries[elementSelector]!
+          if (!toElementEntries) {
+            toElementEntries = scrollRestorationCache.state[cacheKey] =
+              {} as ScrollRestorationByElement
           }
+
+          toElementEntries[elementSelector] ??=
+            fromElementEntries[elementSelector]!
         }
       }
+    }
+
+    if (!shouldResetScroll) {
+      return
+    }
+
+    ignoreScroll = true
+
+    try {
+      const hash = event.toLocation.hash
+      const hashScrollIntoViewOptions =
+        event.toLocation.state.__hashScrollIntoViewOptions ?? true
+      const action = getLocationHistoryAction(event.toLocation)
+      const skipWindowRestore =
+        hash &&
+        hashScrollIntoViewOptions &&
+        (action === 'PUSH' || action === 'REPLACE')
 
       const elementEntries = router.isScrollRestoring
         ? scrollRestorationCache?.state[cacheKey]

From 1f88ce8feab15d8de1a9c0da82a128bd86abdef3 Mon Sep 17 00:00:00 2001
From: Manuel Schiller <manuel.schiller@caligano.de>
Date: Wed, 20 May 2026 22:09:51 +0200
Subject: [PATCH 5/6] bundlesize optimization

---
 packages/router-core/src/router.ts            |  26 +--
 .../router-core/src/scroll-restoration.ts     | 195 ++++++++----------
 2 files changed, 90 insertions(+), 131 deletions(-)

diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts
index 8ce3866c6c..8b0200e687 100644
--- a/packages/router-core/src/router.ts
+++ b/packages/router-core/src/router.ts
@@ -887,22 +887,10 @@ export function getLocationChangeInfo(
   return { fromLocation, toLocation, pathChanged, hrefChanged, hashChanged }
 }
 
-const locationHistoryActions = new WeakMap<ParsedLocation, HistoryAction>()
-
-export function getLocationHistoryAction(location: ParsedLocation) {
-  return locationHistoryActions.get(location)
-}
-
-function setLocationHistoryAction(
-  location: ParsedLocation,
-  action: HistoryAction | undefined,
-) {
-  if (action) {
-    locationHistoryActions.set(location, action)
-  } else {
-    locationHistoryActions.delete(location)
-  }
-}
+export const locationHistoryActions = new WeakMap<
+  ParsedLocation,
+  HistoryAction
+>()
 
 export type CreateRouterFn = <
   TRouteTree extends AnyRoute,
@@ -2446,7 +2434,11 @@ export class RouterCore<
       this.startTransition(async () => {
         try {
           this.beforeLoad()
-          setLocationHistoryAction(this.latestLocation, historyAction)
+          if (historyAction) {
+            locationHistoryActions.set(this.latestLocation, historyAction)
+          } else {
+            locationHistoryActions.delete(this.latestLocation)
+          }
           const next = this.latestLocation
           const prevLocation = this.stores.resolvedLocation.get()
           const locationChangeInfo = getLocationChangeInfo(next, prevLocation)
diff --git a/packages/router-core/src/scroll-restoration.ts b/packages/router-core/src/scroll-restoration.ts
index f2377f8c1b..d27ad28529 100644
--- a/packages/router-core/src/scroll-restoration.ts
+++ b/packages/router-core/src/scroll-restoration.ts
@@ -1,6 +1,5 @@
 import { isServer } from '@tanstack/router-core/isServer'
-import { isPlainObject } from './utils'
-import { getLocationHistoryAction } from './router'
+import { locationHistoryActions } from './router'
 import type { AnyRouter } from './router'
 import type { ParsedLocation } from './location'
 
@@ -10,11 +9,6 @@ type ScrollRestorationByElement = Record<string, ScrollRestorationEntry>
 
 type ScrollRestorationByKey = Record<string, ScrollRestorationByElement>
 
-type ScrollRestorationCache = {
-  readonly state: ScrollRestorationByKey
-  persist: () => void
-}
-
 export type ScrollRestorationOptions = {
   getKey?: (location: ParsedLocation) => string
   scrollBehavior?: ScrollToOptions['behavior']
@@ -22,57 +16,46 @@ export type ScrollRestorationOptions = {
 
 function getSafeSessionStorage() {
   try {
-    return typeof window !== 'undefined' &&
-      typeof window.sessionStorage === 'object'
-      ? window.sessionStorage
-      : undefined
+    // Accessing sessionStorage itself can throw SecurityError in locked-down
+    // contexts, e.g. sandboxed/opaque origins or blocked storage policies.
+    return sessionStorage
   } catch {
-    // silent
-    return undefined
+    return
   }
 }
 
 // SessionStorage key used to store scroll positions across navigations.
 export const storageKey = 'tsr-scroll-restoration-v1_3'
+const safeSessionStorage = getSafeSessionStorage()
 
-function createScrollRestorationCache(): ScrollRestorationCache | null {
-  const safeSessionStorage = getSafeSessionStorage()
-  if (!safeSessionStorage) {
-    return null
-  }
-
-  let state: ScrollRestorationByKey = {}
-
+function createScrollRestorationCache() {
   try {
-    const parsed = JSON.parse(safeSessionStorage.getItem(storageKey) || '{}')
-    if (isPlainObject(parsed)) {
-      state = parsed as ScrollRestorationByKey
-    }
+    return JSON.parse(
+      safeSessionStorage?.getItem(storageKey) || '{}',
+    ) as ScrollRestorationByKey
   } catch {
     // ignore invalid session storage payloads
+    return {}
   }
+}
 
-  const persist = () => {
-    try {
-      safeSessionStorage.setItem(storageKey, JSON.stringify(state))
-    } catch {
-      if (process.env.NODE_ENV !== 'production') {
-        console.warn(
-          '[ts-router] Could not persist scroll restoration state to sessionStorage.',
-        )
-      }
+function persistScrollRestorationCache() {
+  try {
+    safeSessionStorage?.setItem(
+      storageKey,
+      JSON.stringify(scrollRestorationCache),
+    )
+  } catch {
+    if (process.env.NODE_ENV !== 'production') {
+      console.warn(
+        '[ts-router] Could not persist scroll restoration state to sessionStorage.',
+      )
     }
   }
-
-  return {
-    get state() {
-      return state
-    },
-    persist,
-  }
 }
 
-const scrollRestorationCache = createScrollRestorationCache()
+const scrollRestorationCache = /* @__PURE__ */ createScrollRestorationCache()
+const scrollRestorationIdAttribute = 'data-scroll-restoration-id'
 
 /**
  * The default `getKey` function for `useScrollRestoration`.
@@ -90,18 +73,23 @@ function getScrollRestorationSelector(element: Element): string {
     return `[${scrollRestorationIdAttribute}="${attrId}"]`
   }
 
-  const path = []
+  let selector = ''
   let el: any = element
   let parent: HTMLElement
 
   while ((parent = el.parentNode)) {
-    path.push(
-      `${el.tagName}:nth-child(${Array.prototype.indexOf.call(parent.children, el) + 1})`,
-    )
+    let index = 1
+    let sibling = el
+    while ((sibling = sibling.previousElementSibling)) {
+      index++
+    }
+
+    const part = `${el.localName}:nth-child(${index})`
+    selector = selector ? `${part} > ${selector}` : part
     el = parent
   }
 
-  return `${path.reverse().join(' > ')}`.toLowerCase()
+  return selector
 }
 
 export function getElementScrollRestorationEntry(
@@ -121,11 +109,14 @@ export function getElementScrollRestorationEntry(
 ): ScrollRestorationEntry | undefined {
   const getKey = options.getKey || defaultGetScrollRestorationKey
   const restoreKey = getKey(router.latestLocation)
+  const entries = scrollRestorationCache[restoreKey]
+
+  if (!entries) {
+    return
+  }
 
   if (options.id) {
-    return scrollRestorationCache?.state[restoreKey]?.[
-      `[${scrollRestorationIdAttribute}="${options.id}"]`
-    ]
+    return entries[`[${scrollRestorationIdAttribute}="${options.id}"]`]
   }
 
   const element = options.getElement?.()
@@ -133,16 +124,15 @@ export function getElementScrollRestorationEntry(
     return
   }
 
-  return scrollRestorationCache?.state[restoreKey]?.[
-    element instanceof Window
+  return entries[
+    element === window
       ? windowScrollTarget
-      : getScrollRestorationSelector(element)
+      : getScrollRestorationSelector(element as Element)
   ]
 }
 
 let ignoreScroll = false
 const windowScrollTarget = 'window'
-const scrollRestorationIdAttribute = 'data-scroll-restoration-id'
 type ScrollTarget = typeof windowScrollTarget | Element
 
 function getElement(selector: string | (() => Element | null | undefined)) {
@@ -178,7 +168,7 @@ function getScrollToTopElements(
 export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
   // Keep hash/top scrolling active even when sessionStorage is unavailable.
 
-  if (force ?? router.options.scrollRestoration ?? false) {
+  if (force ?? router.options.scrollRestoration) {
     router.isScrollRestoring = true
   }
 
@@ -192,40 +182,40 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
   const getKey =
     router.options.getScrollRestorationKey || defaultGetScrollRestorationKey
   const trackedScrollEntries = new Map<ScrollTarget, ScrollRestorationEntry>()
+  const setTrackedScrollEntry = (
+    target: ScrollTarget,
+    scrollX: number,
+    scrollY: number,
+  ) => {
+    const entry =
+      trackedScrollEntries.get(target) || ({} as ScrollRestorationEntry)
+    entry.scrollX = scrollX
+    entry.scrollY = scrollY
+    trackedScrollEntries.set(target, entry)
+  }
 
-  window.history.scrollRestoration = 'manual'
+  history.scrollRestoration = 'manual'
 
   const onScroll = (event: Event) => {
     if (ignoreScroll || !router.isScrollRestoring) {
       return
     }
 
-    if (event.target === document || event.target === window) {
-      trackedScrollEntries.set(windowScrollTarget, {
-        scrollX: window.scrollX || 0,
-        scrollY: window.scrollY || 0,
-      })
+    if (event.target === document) {
+      setTrackedScrollEntry(windowScrollTarget, scrollX, scrollY)
     } else {
       const target = event.target as Element
-      trackedScrollEntries.set(target, {
-        scrollX: target.scrollLeft || 0,
-        scrollY: target.scrollTop || 0,
-      })
+      setTrackedScrollEntry(target, target.scrollLeft, target.scrollTop)
     }
   }
 
   // Snapshot the current page's tracked scroll targets before navigation or unload.
-  const snapshotCurrentScrollTargets = (restoreKey?: string) => {
-    if (
-      !router.isScrollRestoring ||
-      !restoreKey ||
-      trackedScrollEntries.size === 0 ||
-      !scrollRestorationCache
-    ) {
+  const snapshotCurrentScrollTargets = (restoreKey: string) => {
+    if (!router.isScrollRestoring) {
       return
     }
 
-    const keyEntry = (scrollRestorationCache.state[restoreKey] ||=
+    const keyEntry = (scrollRestorationCache[restoreKey] ||=
       {} as ScrollRestorationByElement)
 
     for (const [target, position] of trackedScrollEntries) {
@@ -239,18 +229,18 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
 
   document.addEventListener('scroll', onScroll, true)
   router.subscribe('onBeforeLoad', (event) => {
-    snapshotCurrentScrollTargets(
-      event.fromLocation ? getKey(event.fromLocation) : undefined,
-    )
+    if (event.fromLocation) {
+      snapshotCurrentScrollTargets(getKey(event.fromLocation))
+    }
     trackedScrollEntries.clear()
   })
-  window.addEventListener('pagehide', () => {
+  addEventListener('pagehide', () => {
     snapshotCurrentScrollTargets(
       getKey(
         router.stores.resolvedLocation.get() ?? router.stores.location.get(),
       ),
     )
-    scrollRestorationCache?.persist()
+    persistScrollRestorationCache()
   })
 
   // Restore destination scroll after the new route has rendered.
@@ -275,16 +265,11 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
     const cacheKey = getKey(event.toLocation)
     const fromCacheKey = event.fromLocation && getKey(event.fromLocation)
 
-    if (
-      router.isScrollRestoring &&
-      scrollRestorationCache &&
-      fromCacheKey &&
-      fromCacheKey !== cacheKey
-    ) {
-      const fromElementEntries = scrollRestorationCache.state[fromCacheKey]
+    if (router.isScrollRestoring && fromCacheKey && fromCacheKey !== cacheKey) {
+      const fromElementEntries = scrollRestorationCache[fromCacheKey]
 
       if (fromElementEntries) {
-        let toElementEntries = scrollRestorationCache.state[cacheKey]
+        let toElementEntries = scrollRestorationCache[cacheKey]
 
         for (const elementSelector in fromElementEntries) {
           if (elementSelector === windowScrollTarget) {
@@ -307,7 +292,7 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
           }
 
           if (!toElementEntries) {
-            toElementEntries = scrollRestorationCache.state[cacheKey] =
+            toElementEntries = scrollRestorationCache[cacheKey] =
               {} as ScrollRestorationByElement
           }
 
@@ -327,50 +312,37 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
       const hash = event.toLocation.hash
       const hashScrollIntoViewOptions =
         event.toLocation.state.__hashScrollIntoViewOptions ?? true
-      const action = getLocationHistoryAction(event.toLocation)
+      const action = locationHistoryActions.get(event.toLocation)
       const skipWindowRestore =
         hash &&
         hashScrollIntoViewOptions &&
         (action === 'PUSH' || action === 'REPLACE')
 
       const elementEntries = router.isScrollRestoring
-        ? scrollRestorationCache?.state[cacheKey]
+        ? scrollRestorationCache[cacheKey]
         : undefined
       let windowRestored = false
 
       if (elementEntries) {
         for (const elementSelector in elementEntries) {
-          const entry = elementEntries[elementSelector]
-
-          if (!isPlainObject(entry)) {
-            continue
-          }
-
-          const { scrollX, scrollY } = entry as {
-            scrollX?: unknown
-            scrollY?: unknown
-          }
-
-          if (!Number.isFinite(scrollX) || !Number.isFinite(scrollY)) {
-            continue
-          }
+          const { scrollX, scrollY } = elementEntries[elementSelector]!
 
           if (elementSelector === windowScrollTarget) {
             if (skipWindowRestore) {
               continue
             }
 
-            window.scrollTo({
-              top: scrollY as number,
-              left: scrollX as number,
+            scrollTo({
+              top: scrollY,
+              left: scrollX,
               behavior,
             })
             windowRestored = true
           } else {
             const element = getElement(elementSelector)
             if (element) {
-              element.scrollLeft = scrollX as number
-              element.scrollTop = scrollY as number
+              element.scrollLeft = scrollX
+              element.scrollTop = scrollY
             }
           }
         }
@@ -390,7 +362,7 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
             behavior,
           }
 
-          window.scrollTo(scrollOptions)
+          scrollTo(scrollOptions)
           if (scrollToTopSelectors) {
             scrollToTopElements ??= getScrollToTopElements(scrollToTopSelectors)
             for (const element of scrollToTopElements) {
@@ -402,10 +374,5 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
     } finally {
       ignoreScroll = false
     }
-
-    if (router.isScrollRestoring && scrollRestorationCache) {
-      scrollRestorationCache.state[cacheKey] ||=
-        {} as ScrollRestorationByElement
-    }
   })
 }

From 47bdd94ff95cb03c2513451eb31ee1efb129e397 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 20 May 2026 20:52:14 +0000
Subject: [PATCH 6/6] test(scroll-restoration): use direct zod schema for
 validateSearch

Agent-Logs-Url: https://github.com/TanStack/router/sessions/bb5df371-ba70-4734-9012-8862a958211b

Co-authored-by: schiller-manuel <6340397+schiller-manuel@users.noreply.github.com>
---
 .../src/routes/(tests)/nested-scroll-search.tsx          | 9 +++------
 1 file changed, 3 insertions(+), 6 deletions(-)

diff --git a/e2e/react-start/scroll-restoration/src/routes/(tests)/nested-scroll-search.tsx b/e2e/react-start/scroll-restoration/src/routes/(tests)/nested-scroll-search.tsx
index c9889386f2..7216ba421c 100644
--- a/e2e/react-start/scroll-restoration/src/routes/(tests)/nested-scroll-search.tsx
+++ b/e2e/react-start/scroll-restoration/src/routes/(tests)/nested-scroll-search.tsx
@@ -1,13 +1,10 @@
 import { Link, createFileRoute } from '@tanstack/react-router'
 import { z } from 'zod'
-import { zodValidator } from '@tanstack/zod-adapter'
 
 export const Route = createFileRoute('/(tests)/nested-scroll-search')({
-  validateSearch: zodValidator(
-    z.object({
-      query: z.string().optional(),
-    }),
-  ),
+  validateSearch: z.object({
+    query: z.string().optional(),
+  }),
   component: Component,
 })
 
