Domphy

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" })
  },
})

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

MethodDescription
router.subscribe("onBeforeNavigate", fn)Fires before navigation starts
router.subscribe("onNavigated", fn)Fires after navigation completes
toLocation.hashURL hash ("#section" or "")
historyAction"push" / "pop" / "replace"