Skip to content

Add a scroll option to Navigation.navigate() and <Link>#975

Merged
lemonmade merged 2 commits into
mainfrom
navigate-scroll-option
Jul 14, 2026
Merged

Add a scroll option to Navigation.navigate() and <Link>#975
lemonmade merged 2 commits into
mainfrom
navigate-scroll-option

Conversation

@lemonmade

@lemonmade lemonmade commented Jul 14, 2026

Copy link
Copy Markdown
Owner

Adds a scroll option to Navigation.navigate() and <Link>, and makes the URL change — not the history operation — decide the default scroll behavior.

Default behavior change

For push and replace navigations alike:

  • Changing the pathname or hash resets the scroll position to the top of the page (or scrolls to the URL's hash target when present). Previously, replace navigations never reset.
  • Changing only the search params keeps the current scroll position. Previously, push navigations always reset — which yanked the page to the top for URL-encoded UI state like filters, tabs, or a selected calendar day.

Back/forward still restores each entry's saved offset, and reloads still restore the entry's offset.

This differs deliberately from React Router / Next.js / TanStack Router, which reset on search-only changes too (behind preventScrollReset / scroll: false / resetScroll: false opt-outs). Search-only navigations almost always encode UI state rather than a new location, so staying put is the default that matches intent; the scroll option covers the exceptions in both directions.

Previously, apps reached for replace to get its keep-scroll side effect. Now push/replace is purely a history-semantics choice, and scroll behavior follows the shape of the URL change.

The scroll option

// Force a search-only navigation to reset:
navigation.navigate(url, {scroll: true});

// Keep the position on a path change:
navigation.navigate(url, {replace: true, scroll: false});
<Link to={url} scroll={false}>
  Select day
</Link>

With scroll: false, the kept offset is recorded against the new history entry, so a later back/forward return to it still restores correctly. The option is a no-op when the Navigation was constructed with scrollRestoration: false.

Tests

New packages/preact-router/source/Navigation.test.ts covers: manual-restoration setup, scrollRestoration: false, push/replace top reset on path changes, search-only keep (push and replace), scroll: true forcing a reset, scroll: false keeping the position (push and replace), offset recording under scroll: false, hash-target scrolling (path change and same-path hash change), popstate restore, and sessionStorage persistence.

pnpm vitest run --config configuration/vite.unit.config.ts packages/preact-router/source/Navigation.test.ts — 14/14 pass. tsc --build packages/preact-router — clean.

Note: packages/preact-router/source/components/Link.test.tsx currently fails to transform on main with the pinned toolchain (rolldown can't parse esbuild's lowering of using declarations) — pre-existing and unrelated to this change.

🤖 Generated with Claude Code

Chris Sauve and others added 2 commits July 14, 2026 15:17
Aligns the default scroll behavior with React Router, Next.js, and
TanStack Router: every forward navigation (push or replace) resets the
scroll position to the top of the page, or to the URL's hash target.
Previously, replace navigations kept the page where it was.

Pass `scroll: false` (or `<Link scroll={false}>`) to keep the current
scroll position - useful when the URL encodes UI state (filters, tabs,
a selected calendar day) and the page should stay put. The kept offset
is recorded against the new history entry so a later back/forward
return still restores it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The URL change, not the history operation, now decides the default:
push and replace both reset on a pathname/hash change and both stay
put when only the search params change. Search-only navigations
almost always encode UI state (filters, tabs, a selected day) rather
than a new location. `scroll: true` / `scroll: false` force either
behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lemonmade
lemonmade merged commit 6e61707 into main Jul 14, 2026
8 checks passed
@lemonmade lemonmade mentioned this pull request Jul 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant