Problem
When an app sets html { scroll-behavior: smooth } (a reasonable choice for in-page anchor links), the client router's navigation scroll-restoration becomes visibly animated instead of instant. The user watches the page scroll.
Reproduced on https://example-blog.webjs.dev :
- Scroll down the home page, click the "Hello World!" post link, and the new page visibly scrolls to the top instead of starting at the top.
- Click the browser Back button, and the home page visibly scrolls from the top down to the prior link position instead of restoring instantly.
Root cause: the router's programmatic scroll calls use the 2-argument window.scrollTo(x, y) form, which respects the CSS scroll-behavior. Native full-page navigation does not show this (the browser restores scroll before first paint), but the SPA router scrolls AFTER swapping the DOM, so the smooth rule animates it. Measured on the live site: a scrollTo(0,1500) climbs roughly 482, 675, 987, 1135, up to 1320 over about 400ms instead of jumping.
Design / approach
Force an explicit instant behavior at the nav scroll-restoration call sites so the router overrides the page's scroll-behavior and matches native navigation. Verified on the live site that the options form jumps instantly even with html{scroll-behavior:smooth} active:
window.scrollTo({ top: 0, left: 0, behavior: 'instant' });
window.scrollTo({ top: cached.scrollX, left: cached.scrollY, behavior: 'instant' });
Leave the explicit hash-anchor scroll (t.scrollIntoView() at ~L1625) ALONE. A #section link is exactly the case where smooth scrolling is desired, and native browsers animate it too.
Implementation notes (for the implementing agent)
- Where to edit:
packages/core/src/router-client.js, three scroll-restoration call sites.
- ~L805 popstate cache-hit restore:
window.scrollTo(cached.scrollX, cached.scrollY) becomes the options form with behavior: 'instant'. Note the existing args: it passes scrollX as top and scrollY as left. Preserve that mapping exactly, do not "fix" it as part of this change. If it is actually transposed, that is a separate issue.
- ~L821 cache-miss popstate scroll-to-top:
window.scrollTo(0, 0).
- ~L1626 / ~L1628 forward-nav scroll-to-top (inside
fetchAndApply, the recordHistory block, both the no-hash branch and the hash-miss else): window.scrollTo(0, 0).
- Do NOT change the
t.scrollIntoView() hash-anchor branch at ~L1625.
- Landmine:
behavior: 'instant' (not 'auto') is required. 'auto' resolves to the computed CSS scroll-behavior, so it would still animate under a smooth stylesheet. 'instant' forces a jump regardless. Confirmed on the live blog.
- Landmine: the 2-arg
scrollTo(x, y) and the 1-arg options scrollTo({top, left, behavior}) are different overloads. Move to the options object, do not pass a third positional arg.
- Invariant:
packages/ is buildless plain JS plus JSDoc (root AGENTS.md). No .ts in core src.
Acceptance criteria
Problem
When an app sets
html { scroll-behavior: smooth }(a reasonable choice for in-page anchor links), the client router's navigation scroll-restoration becomes visibly animated instead of instant. The user watches the page scroll.Reproduced on https://example-blog.webjs.dev :
Root cause: the router's programmatic scroll calls use the 2-argument
window.scrollTo(x, y)form, which respects the CSSscroll-behavior. Native full-page navigation does not show this (the browser restores scroll before first paint), but the SPA router scrolls AFTER swapping the DOM, so the smooth rule animates it. Measured on the live site: ascrollTo(0,1500)climbs roughly 482, 675, 987, 1135, up to 1320 over about 400ms instead of jumping.Design / approach
Force an explicit instant behavior at the nav scroll-restoration call sites so the router overrides the page's
scroll-behaviorand matches native navigation. Verified on the live site that the options form jumps instantly even withhtml{scroll-behavior:smooth}active:Leave the explicit hash-anchor scroll (
t.scrollIntoView()at ~L1625) ALONE. A#sectionlink is exactly the case where smooth scrolling is desired, and native browsers animate it too.Implementation notes (for the implementing agent)
packages/core/src/router-client.js, three scroll-restoration call sites.window.scrollTo(cached.scrollX, cached.scrollY)becomes the options form withbehavior: 'instant'. Note the existing args: it passesscrollXastopandscrollYasleft. Preserve that mapping exactly, do not "fix" it as part of this change. If it is actually transposed, that is a separate issue.window.scrollTo(0, 0).fetchAndApply, therecordHistoryblock, both the no-hash branch and the hash-misselse):window.scrollTo(0, 0).t.scrollIntoView()hash-anchor branch at ~L1625.behavior: 'instant'(not'auto') is required.'auto'resolves to the computed CSSscroll-behavior, so it would still animate under a smooth stylesheet.'instant'forces a jump regardless. Confirmed on the live blog.scrollTo(x, y)and the 1-arg optionsscrollTo({top, left, behavior})are different overloads. Move to the options object, do not pass a third positional arg.packages/is buildless plain JS plus JSDoc (root AGENTS.md). No.tsin core src.Acceptance criteria
html { scroll-behavior: smooth }packages/core/test/routing/browser/drives a navigation under ascroll-behavior: smoothstylesheet and asserts scroll position settles within one frame (no multi-frame animation ramp)scrollToand passes after the fix