Scroll Restoration

Scroll restoration is the process of restoring the scroll position of a page when the user navigates back to it. This is normally a built-in feature for standard HTML based websites, but can be difficult to replicate for SPA applications because:

  • SPAs typically use the history.pushState API for navigation, so the browser doesn't know to restore the scroll position natively
  • SPAs sometimes render content asynchronously, so the browser doesn't know the height of the page until after it's rendered
  • SPAs can sometimes use nested scrollable containers to force specific layouts and features.

Not only that, but it's very common for applications to have multiple scrollable areas within an app, not just the body. For example, a chat application might have a scrollable sidebar and a scrollable chat area. In this case, you would want to restore the scroll position of both areas independently.

To alleviate this problem, TanStack Router provides a scroll restoration component and hook that handle the process of monitoring, caching and restoring scroll positions for you.

It does this by:

  • Monitoring the DOM for scroll events
  • Registering scrollable areas with the scroll restoration cache
  • Listening to the proper router events to know when to cache and restore scroll positions
  • Storing scroll positions for each scrollable area in the cache (including window and body)
  • Restoring scroll positions after successful navigations before DOM paint

That may sound like a lot, but for you, it's as simple as this:

tsx
import { ScrollRestoration } from '@tanstack/react-router'

function Root() {
  return (
    <>
      <ScrollRestoration />
      <Outlet />
    </>
  )
}
import { ScrollRestoration } from '@tanstack/react-router'

function Root() {
  return (
    <>
      <ScrollRestoration />
      <Outlet />
    </>
  )
}

Just render the ScrollRestoration component (or use the useScrollRestoration hook) at the root of your application and it will handle everything automatically!

Custom Cache Keys

Falling in behind Remix's own Scroll Restoration APIs, you can also customize the key used to cache scroll positions for a given scrollable area using the getKey option. This could be used, for example, to force the same scroll position to be used regardless of the users browser history.

The getKey option receives the relevant Location state from TanStack Router and expects you to return a string to uniquely identify the scrollable measurements for that state.

The default getKey is (location) => location.state.key!, where key is the unique key generated for each entry in the history.

Examples

You could sync scrolling to the pathname:

tsx
import { ScrollRestoration } from '@tanstack/react-router'

function Root() {
  return (
    <>
      <ScrollRestoration getKey={(location) => location.pathname} />
      <Outlet />
    </>
  )
}
import { ScrollRestoration } from '@tanstack/react-router'

function Root() {
  return (
    <>
      <ScrollRestoration getKey={(location) => location.pathname} />
      <Outlet />
    </>
  )
}

You can conditionally sync only some paths, then use the key for the rest:

tsx
import { ScrollRestoration } from '@tanstack/react-router'

function Root() {
  return (
    <>
      <ScrollRestoration
        getKey={(location) => {
          const paths = ['/', '/chat']
          return paths.includes(location.pathname)
            ? location.pathname
            : location.state.key!
        }}
      />
      <Outlet />
    </>
  )
}
import { ScrollRestoration } from '@tanstack/react-router'

function Root() {
  return (
    <>
      <ScrollRestoration
        getKey={(location) => {
          const paths = ['/', '/chat']
          return paths.includes(location.pathname)
            ? location.pathname
            : location.state.key!
        }}
      />
      <Outlet />
    </>
  )
}

Preventing Scroll Restoration

Sometimes you may want to prevent scroll restoration from happening. To do this you can utilize the resetScroll option available on the following APIs:

  • <Link resetScroll={false}>
  • navigate({ resetScroll: false })
  • redirect({ resetScroll: false })

When resetScroll is set to false, the scroll position for the next navigation will not be restored (if navigating to an existing history event in the stack) or reset to the top (if it's a new history event in the stack).

Manual Scroll Restoration

Most of the time, you won't need to do anything special to get scroll restoration to work. However, there are some cases where you may need to manually control scroll restoration. The most common example is virtualized lists.

To manually control scroll restoration, you can use the useElementScrollRestoration hook and the data-scroll-restoration-id DOM attribute:

tsx
function Component() {
  // We need a unique ID for manual scroll restoration on a specific element
  // It should be as unique as possible for this element across your app
  const scrollRestorationId = 'myVirtualizedContent'

  // We use that ID to get the scroll entry for this element
  const scrollEntry = useElementScrollRestoration({
    id: scrollRestorationId,
  })

  // Let's use TanStack Virtual to virtualize some content!
  const virtualizerParentRef = React.useRef<HTMLDivElement>(null)
  const virtualizer = useVirtualizer({
    count: 10000,
    getScrollElement: () => virtualizerParentRef.current,
    estimateSize: () => 100,
    // We pass the scrollY from the scroll restoration entry to the virtualizer
    // as the initial offset
    initialOffset: scrollEntry?.scrollY,
  })

  return (
    <div
      ref={virtualizerParentRef}
      // We pass the scroll restoration ID to the element
      // as a custom attribute that will get picked up by the
      // scroll restoration watcher
      data-scroll-restoration-id={scrollRestorationId}
      className="flex-1 border rounded-lg overflow-auto relative"
    >
      ...
    </div>
  )
}
function Component() {
  // We need a unique ID for manual scroll restoration on a specific element
  // It should be as unique as possible for this element across your app
  const scrollRestorationId = 'myVirtualizedContent'

  // We use that ID to get the scroll entry for this element
  const scrollEntry = useElementScrollRestoration({
    id: scrollRestorationId,
  })

  // Let's use TanStack Virtual to virtualize some content!
  const virtualizerParentRef = React.useRef<HTMLDivElement>(null)
  const virtualizer = useVirtualizer({
    count: 10000,
    getScrollElement: () => virtualizerParentRef.current,
    estimateSize: () => 100,
    // We pass the scrollY from the scroll restoration entry to the virtualizer
    // as the initial offset
    initialOffset: scrollEntry?.scrollY,
  })

  return (
    <div
      ref={virtualizerParentRef}
      // We pass the scroll restoration ID to the element
      // as a custom attribute that will get picked up by the
      // scroll restoration watcher
      data-scroll-restoration-id={scrollRestorationId}
      className="flex-1 border rounded-lg overflow-auto relative"
    >
      ...
    </div>
  )
}
Subscribe to Bytes

Your weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.

Bytes

No spam. Unsubscribe at any time.