history-scroll-restoration-contracts
Installation
SKILL.md
History and scroll restoration contracts
Scroll restoration is a coordination contract between the session-history entry, the scroll owner, and layout readiness. A saved offset can be correct while restoration still fails because it is applied to the wrong entry or container, before the destination has enough scrollable extent, or again after the browser already restored it.
Checklist
- Reproduce the exact navigation shape in a real browser: link navigation,
history.back()/Back, Forward,navigate(-1), or same-document hash traversal. Record the URL, history-entry identity or router key, navigation action, scroll owner, saved offset, applied offset, and final offset after async content settles. - Decide who owns restoration. Leave
history.scrollRestorationasautowhen browser restoration is the product contract. Usemanualonly when the application or router owns the whole save-and-restore path. Competing browser and application writes can create a second jump or make success timing-dependent. - Key positions by the intended history-entry semantics. A unique location key preserves separate positions for repeated visits to the same pathname; pathname-based keys intentionally reuse a position. Do not silently switch between these contracts or collapse query/hash-distinct entries without a product reason.
- Identify the actual scroll owner.
window.scrollYdoes not represent a nestedoverflow: autoroute shell. Save and restore each intended container explicitly, and verify that a remounted container is the same logical owner before applying an old offset. - Restore only when the destination can represent the saved position. If async loaders, images, fonts, suspense boundaries, or conditional sections change the scroll extent, an immediate
scrollTo()can be clamped and look successful in logs while landing too high. Tie restoration to an observable route/data/layout readiness boundary, then confirm the offset remains stable after the content above it finishes changing. - Do not replace readiness with an arbitrary delay or an unbounded retry loop. A fixed timeout is device- and network-dependent; repeated writes fight user input and browser scroll anchoring. Bound any retry by the destination entry and stop after success, user scroll, route change, or a documented timeout.
- Treat fragments as navigation state, not just string parsing. Verify that Back/Forward between hash entries reaches the intended target after it exists. When the fragment is an accessibility navigation target, verify focus separately instead of treating correct scrolling as evidence of correct focus.
- Keep scroll anchoring enabled by default.
overflow-anchorcan reduce content-shift jumps; disable it only on the smallest demonstrated region when anchoring itself conflicts with the explicit restoration contract. - Cover new entries, not only traversals. A programmatic push or replace (a redirect,
router.replace, a guard bouncing to another route) creates a destination entry with no saved position of its own, and a router may reset it by default — for example, the Next.js Pages Routerrouter.push/router.replacescrolloption scrolls to the top and defaults totrue. A new entry that keeps a stale offset instead points to that reset being turned off, a nested scroll owner the router does not reset, or an application restore keyed to the wrong entry (item 3). Record the navigation as push/replace, not traversal, and assert the owner starts at the top or at the fragment target.