Skip to content

Add scroll restoration to the preact-router Navigation#939

Merged
lemonmade merged 1 commit into
mainfrom
claude/scroll-restoration
May 25, 2026
Merged

Add scroll restoration to the preact-router Navigation#939
lemonmade merged 1 commit into
mainfrom
claude/scroll-restoration

Conversation

@lemonmade
Copy link
Copy Markdown
Owner

Summary

Navigation now manages scroll position across navigations, fixing the
common SPA bug where client-side navigation lands at the wrong scroll offset.

The browser's native scroll restoration runs against the document as it exists
at popstate time, but an async route hasn't rendered its content yet — so a
back/forward restore lands against the wrong (usually shorter) document. Forward
navigations are also frequently left at the previous page's offset rather than
the top.

In the browser, Navigation now:

  • sets history.scrollRestoration = 'manual' and keeps its own per-entry scroll
    offsets, keyed by navigation id and persisted to sessionStorage (so they
    survive a reload within the tab session, capped at 50 entries);
  • forward navigation → resets to the top, or scrolls to the URL hash target
    when present;
  • back/forward navigation → restores the offset the entry was last left at;
  • reload → restores the offset of the entry it lands on.

Restores that need the destination route's content committed (a saved offset or
a hash target) are applied on the next animation frame; a plain reset to the top
is applied synchronously to avoid a flash of the new route at the previous
offset. All of this is window/document-scoped and fully guarded for non-browser
(SSR) construction.

Enabled by default. Pass scrollRestoration: false to leave scrolling to the
browser/your app:

const navigation = new Navigation(initialURL, {scrollRestoration: false});

A changeset (minor bump for @quilted/preact-router) is included.

Test plan

  • Added Navigation scroll-restoration unit tests (jsdom): manual mode by
    default, opt-out, reset-to-top on forward nav, restore-on-back, and
    sessionStorage persistence keyed by navigation id.
  • tsc --build packages/preact-router/tsconfig.json passes.
  • prettier --check clean.
  • pnpm test in CI (the equivalent suite was validated against the built
    output in a downstream consumer; CI builds the workspace tooling first).

In a single-page app the browser's native scroll restoration is unreliable:
on a back/forward navigation it restores against the document as it exists at
popstate time, but an async route hasn't rendered yet, so it lands at the wrong
offset; forward navigations are often left at the previous page's offset.

Navigation now owns it. In the browser it sets history.scrollRestoration to
'manual' and keeps per-entry offsets keyed by navigation id, persisted to
sessionStorage: forward resets to the top (or the URL hash target), back/forward
restores the offset the entry was left at, and a reload restores its landing
entry. Restores that need committed content (a saved offset or hash target) run
on the next frame; a plain reset to top is synchronous to avoid a flash. Enabled
by default; pass scrollRestoration:false to opt out.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@lemonmade lemonmade merged commit 908f1ad into main May 25, 2026
7 checks passed
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