Scroll Restoration
Default behavior
@domphy/router does not automatically restore scroll position — scroll behavior is explicit and composable. The most common setup scrolls to the top on every navigation:
import { createRouter } from "@domphy/router"
const router = createRouter({
routeTree,
scrollRestoration: false, // manual control (default)
})
// Scroll to top on route change
router.subscribe("onBeforeNavigate", () => {
window.scrollTo({ top: 0, behavior: "instant" })
})Scroll restoration on back/forward
Save the scroll position before leaving a route, restore it when returning:
const SCROLL_KEY = "router-scroll-positions"
function saveScroll(key: string) {
const positions = JSON.parse(sessionStorage.getItem(SCROLL_KEY) ?? "{}")
positions[key] = window.scrollY
sessionStorage.setItem(SCROLL_KEY, JSON.stringify(positions))
}
function restoreScroll(key: string) {
const positions = JSON.parse(sessionStorage.getItem(SCROLL_KEY) ?? "{}")
const saved = positions[key]
if (saved != null) {
requestAnimationFrame(() => window.scrollTo({ top: saved, behavior: "instant" }))
}
}
router.subscribe("onBeforeNavigate", ({ toLocation, fromLocation }) => {
if (fromLocation?.pathname) {
saveScroll(fromLocation.pathname)
}
})
router.subscribe("onNavigated", ({ toLocation, historyAction }) => {
if (historyAction === "pop") {
// Back/forward — restore
restoreScroll(toLocation.pathname)
} else {
// New navigation — scroll to top
window.scrollTo({ top: 0, behavior: "instant" })
}
})Per-route scroll behavior
Override scroll for specific routes:
const postRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/posts/$postId",
component: () => PostPage,
// Scroll to top when entering any post
onEnter: () => window.scrollTo({ top: 0 }),
})
const commentsRoute = createRoute({
getParentRoute: () => postRoute,
path: "comments",
// Scroll to the comments section
onEnter: () => {
document.getElementById("comments")?.scrollIntoView({ behavior: "smooth" })
},
})Hash link scrolling
Scroll to an element when the URL has a #anchor:
router.subscribe("onNavigated", ({ toLocation }) => {
const hash = toLocation.hash
if (hash) {
// Wait for DOM to render before scrolling
requestAnimationFrame(() => {
const target = document.getElementById(hash.slice(1))
target?.scrollIntoView({ behavior: "smooth", block: "start" })
})
} else {
window.scrollTo({ top: 0, behavior: "instant" })
}
})Link to a specific section:
import { link } from "@domphy/router/domphy"
const ApiLink = {
a: "API Reference",
href: "/docs/core/#api-reference",
$: [link({ to: "/docs/core/", hash: "#api-reference" })],
}Infinite scroll with virtual list — scroll restoration
For pages with a virtual infinite scroll list, save/restore the scroll offset of the container element:
import { toState } from "@domphy/core"
import { createRoute } from "@domphy/router"
const feedContainer = toState<HTMLElement | null>(null)
const feedRoute = createRoute({
path: "/feed",
component: () => FeedPage,
onLeave: () => {
const el = feedContainer.get()
if (el) sessionStorage.setItem("feed-scroll", String(el.scrollTop))
},
onEnter: () => {
requestAnimationFrame(() => {
const el = feedContainer.get()
const saved = sessionStorage.getItem("feed-scroll")
if (el && saved) el.scrollTop = Number(saved)
})
},
})Blocking scroll during transitions
For page transitions (fade/slide), prevent scroll jump mid-animation:
import { toState } from "@domphy/core"
const isTransitioning = toState(false)
router.subscribe("onBeforeNavigate", () => {
isTransitioning.set(true)
document.body.style.overflow = "hidden"
})
router.subscribe("onNavigated", () => {
setTimeout(() => {
isTransitioning.set(false)
document.body.style.overflow = ""
window.scrollTo({ top: 0, behavior: "instant" })
}, 300) // match your CSS transition duration
})Route-level scroll position API
| Method | Description |
|---|---|
router.subscribe("onBeforeNavigate", fn) | Fires before navigation starts |
router.subscribe("onNavigated", fn) | Fires after navigation completes |
toLocation.hash | URL hash ("#section" or "") |
historyAction | "push" / "pop" / "replace" |