diff --git a/packages/e2e/next/cypress/e2e/shared/scroll.cy.ts b/packages/e2e/next/cypress/e2e/shared/scroll.cy.ts
new file mode 100644
index 00000000..3f0af77e
--- /dev/null
+++ b/packages/e2e/next/cypress/e2e/shared/scroll.cy.ts
@@ -0,0 +1,11 @@
+import { testScroll } from 'e2e-shared/specs/scroll.cy'
+
+testScroll({
+ path: '/app/scroll',
+ nextJsRouter: 'app'
+})
+
+testScroll({
+ path: '/pages/scroll',
+ nextJsRouter: 'pages'
+})
diff --git a/packages/e2e/next/src/app/app/(shared)/scroll/page.tsx b/packages/e2e/next/src/app/app/(shared)/scroll/page.tsx
new file mode 100644
index 00000000..3c874986
--- /dev/null
+++ b/packages/e2e/next/src/app/app/(shared)/scroll/page.tsx
@@ -0,0 +1,10 @@
+import { Scroll } from 'e2e-shared/specs/scroll'
+import { Suspense } from 'react'
+
+export default function Page() {
+ return (
+
+
+
+ )
+}
diff --git a/packages/e2e/next/src/pages/pages/scroll.tsx b/packages/e2e/next/src/pages/pages/scroll.tsx
new file mode 100644
index 00000000..b857ba37
--- /dev/null
+++ b/packages/e2e/next/src/pages/pages/scroll.tsx
@@ -0,0 +1,3 @@
+import { Scroll } from 'e2e-shared/specs/scroll'
+
+export default Scroll
diff --git a/packages/e2e/react-router/v6/cypress/e2e/shared/scroll.cy.ts b/packages/e2e/react-router/v6/cypress/e2e/shared/scroll.cy.ts
new file mode 100644
index 00000000..e1801854
--- /dev/null
+++ b/packages/e2e/react-router/v6/cypress/e2e/shared/scroll.cy.ts
@@ -0,0 +1,3 @@
+import { testScroll } from 'e2e-shared/specs/scroll.cy'
+
+testScroll({ path: '/scroll' })
diff --git a/packages/e2e/react-router/v6/src/react-router.tsx b/packages/e2e/react-router/v6/src/react-router.tsx
index 64fe6f4b..6d007b93 100644
--- a/packages/e2e/react-router/v6/src/react-router.tsx
+++ b/packages/e2e/react-router/v6/src/react-router.tsx
@@ -45,6 +45,7 @@ const router = createBrowserRouter(
+
diff --git a/packages/e2e/react-router/v6/src/routes/scroll.tsx b/packages/e2e/react-router/v6/src/routes/scroll.tsx
new file mode 100644
index 00000000..b857ba37
--- /dev/null
+++ b/packages/e2e/react-router/v6/src/routes/scroll.tsx
@@ -0,0 +1,3 @@
+import { Scroll } from 'e2e-shared/specs/scroll'
+
+export default Scroll
diff --git a/packages/e2e/react-router/v7/app/routes.ts b/packages/e2e/react-router/v7/app/routes.ts
index fd9a0a24..0fc04bee 100644
--- a/packages/e2e/react-router/v7/app/routes.ts
+++ b/packages/e2e/react-router/v7/app/routes.ts
@@ -28,6 +28,7 @@ export default [
route('/fog-of-war/result', './routes/fog-of-war.result.tsx'),
route('/conditional-rendering/useQueryState', './routes/conditional-rendering.useQueryState.tsx'),
route('/conditional-rendering/useQueryStates', './routes/conditional-rendering.useQueryStates.tsx'),
+ route('/scroll', './routes/scroll.tsx'),
route('/render-count/:hook/:shallow/:history/:startTransition/no-loader', './routes/render-count.$hook.$shallow.$history.$startTransition.no-loader.tsx'),
route('/render-count/:hook/:shallow/:history/:startTransition/sync-loader', './routes/render-count.$hook.$shallow.$history.$startTransition.sync-loader.tsx'),
diff --git a/packages/e2e/react-router/v7/app/routes/scroll.tsx b/packages/e2e/react-router/v7/app/routes/scroll.tsx
new file mode 100644
index 00000000..b857ba37
--- /dev/null
+++ b/packages/e2e/react-router/v7/app/routes/scroll.tsx
@@ -0,0 +1,3 @@
+import { Scroll } from 'e2e-shared/specs/scroll'
+
+export default Scroll
diff --git a/packages/e2e/react-router/v7/cypress/e2e/shared/scroll.cy.ts b/packages/e2e/react-router/v7/cypress/e2e/shared/scroll.cy.ts
new file mode 100644
index 00000000..e1801854
--- /dev/null
+++ b/packages/e2e/react-router/v7/cypress/e2e/shared/scroll.cy.ts
@@ -0,0 +1,3 @@
+import { testScroll } from 'e2e-shared/specs/scroll.cy'
+
+testScroll({ path: '/scroll' })
diff --git a/packages/e2e/react/cypress/e2e/shared/scroll.cy.ts b/packages/e2e/react/cypress/e2e/shared/scroll.cy.ts
new file mode 100644
index 00000000..e1801854
--- /dev/null
+++ b/packages/e2e/react/cypress/e2e/shared/scroll.cy.ts
@@ -0,0 +1,3 @@
+import { testScroll } from 'e2e-shared/specs/scroll.cy'
+
+testScroll({ path: '/scroll' })
diff --git a/packages/e2e/react/src/routes.tsx b/packages/e2e/react/src/routes.tsx
index 953fdb5a..13e91c52 100644
--- a/packages/e2e/react/src/routes.tsx
+++ b/packages/e2e/react/src/routes.tsx
@@ -23,6 +23,7 @@ const routes: Record JSX.Element>> = {
'/referential-stability/useQueryStates': lazy(() => import('./routes/referential-stability.useQueryStates')),
'/conditional-rendering/useQueryState': lazy(() => import('./routes/conditional-rendering.useQueryState')),
'/conditional-rendering/useQueryStates': lazy(() => import('./routes/conditional-rendering.useQueryStates')),
+ '/scroll': lazy(() => import('./routes/scroll')),
'/render-count/useQueryState/true/replace/false': lazy(() => import('./routes/render-count')),
'/render-count/useQueryState/true/replace/true': lazy(() => import('./routes/render-count')),
diff --git a/packages/e2e/react/src/routes/scroll.tsx b/packages/e2e/react/src/routes/scroll.tsx
new file mode 100644
index 00000000..b857ba37
--- /dev/null
+++ b/packages/e2e/react/src/routes/scroll.tsx
@@ -0,0 +1,3 @@
+import { Scroll } from 'e2e-shared/specs/scroll'
+
+export default Scroll
diff --git a/packages/e2e/remix/app/routes/scroll.tsx b/packages/e2e/remix/app/routes/scroll.tsx
new file mode 100644
index 00000000..b857ba37
--- /dev/null
+++ b/packages/e2e/remix/app/routes/scroll.tsx
@@ -0,0 +1,3 @@
+import { Scroll } from 'e2e-shared/specs/scroll'
+
+export default Scroll
diff --git a/packages/e2e/remix/cypress/e2e/shared/scroll.cy.ts b/packages/e2e/remix/cypress/e2e/shared/scroll.cy.ts
new file mode 100644
index 00000000..e1801854
--- /dev/null
+++ b/packages/e2e/remix/cypress/e2e/shared/scroll.cy.ts
@@ -0,0 +1,3 @@
+import { testScroll } from 'e2e-shared/specs/scroll.cy'
+
+testScroll({ path: '/scroll' })
diff --git a/packages/e2e/shared/cypress.config.ts b/packages/e2e/shared/cypress.config.ts
index 27382fdd..e5fb55e7 100644
--- a/packages/e2e/shared/cypress.config.ts
+++ b/packages/e2e/shared/cypress.config.ts
@@ -12,7 +12,7 @@ export function defineConfig(config: Config) {
video: false,
fixturesFolder: false,
testIsolation: true,
- defaultCommandTimeout: 500,
+ defaultCommandTimeout: 1000,
setupNodeEvents(on) {
cypressTerminalReport(on)
},
diff --git a/packages/e2e/shared/specs/scroll.cy.ts b/packages/e2e/shared/specs/scroll.cy.ts
new file mode 100644
index 00000000..9cd47cb2
--- /dev/null
+++ b/packages/e2e/shared/specs/scroll.cy.ts
@@ -0,0 +1,18 @@
+import { createTest } from '../create-test'
+
+export const testScroll = createTest('scroll', ({ path }) => {
+ it('does not scroll to the top of the page by default (scroll: false)', () => {
+ cy.visit(path + '?scroll=false')
+ cy.contains('#hydration-marker', 'hydrated').should('be.hidden')
+ cy.get('#not-at-the-top').should('be.visible')
+ cy.get('button').click()
+ cy.get('#not-at-the-top').should('be.visible')
+ })
+ it('scrolls to the top of the page when setting scroll: true', () => {
+ cy.visit(path + '?scroll=true')
+ cy.contains('#hydration-marker', 'hydrated').should('be.hidden')
+ cy.get('#not-at-the-top').should('be.visible')
+ cy.get('button').click()
+ cy.get('#at-the-top').should('be.visible')
+ })
+})
diff --git a/packages/e2e/shared/specs/scroll.tsx b/packages/e2e/shared/specs/scroll.tsx
new file mode 100644
index 00000000..54d965e1
--- /dev/null
+++ b/packages/e2e/shared/specs/scroll.tsx
@@ -0,0 +1,56 @@
+'use client'
+
+import { parseAsBoolean, useQueryState } from 'nuqs'
+import { useEffect, useState } from 'react'
+
+export function Scroll() {
+ return (
+ <>
+
+
+
+ >
+ )
+}
+
+function ScrollDetector() {
+ const [atTheTop, setAtTheTop] = useState(false)
+
+ useEffect(() => {
+ const controller = new AbortController()
+ window.addEventListener('scroll', () => setAtTheTop(window.scrollY === 0), {
+ signal: controller.signal
+ })
+ return () => controller.abort()
+ }, [])
+
+ return (
+
+ {atTheTop ? null : 'not '}at the top
+
+ )
+}
+
+function ScrollAction() {
+ const [scroll] = useQueryState('scroll', parseAsBoolean.withDefault(false))
+ const [, setState] = useQueryState('test', {
+ scroll
+ })
+
+ useEffect(() => {
+ document.getElementById('scroll-to-me')?.scrollIntoView()
+ }, [])
+
+ return (
+
+ )
+}
diff --git a/packages/nuqs/src/adapters/react.ts b/packages/nuqs/src/adapters/react.ts
index 33351b49..5c3f7437 100644
--- a/packages/nuqs/src/adapters/react.ts
+++ b/packages/nuqs/src/adapters/react.ts
@@ -14,6 +14,9 @@ function updateUrl(search: URLSearchParams, options: AdapterOptions) {
options.history === 'push' ? history.pushState : history.replaceState
method.call(history, history.state, '', url)
emitter.emit('update', search)
+ if (options.scroll === true) {
+ window.scrollTo({ top: 0 })
+ }
}
function useNuqsReactAdapter() {