Scroll Position for Astro projects when using the view transitions API

2024-08-08 00:42

When using Astro’s View Transitions API, you might notice that the scroll position isn’t maintained when navigating between pages. This can be particularly noticeable when a user clicks a link, goes to a new page, then uses the browser’s back button. To solve this, I’ve implemented a custom scroll position restoration mechanism on a project.

Just add a script to your Astro layout to handle scroll position restoration:

  1. Open your main layout file (often Layout.astro or similar).

  2. Add the following <script> tag with the is:inline directive:

  • Alternatively you can create a new Astro component and import it

<script is:inline>
  let isBack = false;
  let scrollRestoration = false;

  document.addEventListener("astro:before-preparation", ({ from, to, direction }) => {
    isBack = direction === 'back';
    console.log(`Navigation direction: ${direction}`);
  });

  document.addEventListener("astro:before-swap", () => {
    const currentPath = window.location.pathname;
    const currentScroll = window.scrollY;
    console.log(`Before swap - Current path: ${currentPath}, Scroll position: ${currentScroll}`);

    if (!isBack) {
      const scrollPositions = JSON.parse(sessionStorage.getItem("scrollPositions") || "{}");
      scrollPositions[currentPath] = currentScroll;
      sessionStorage.setItem("scrollPositions", JSON.stringify(scrollPositions));
      console.log(`Saved scroll position for ${currentPath}: ${currentScroll}`);
    } else {
      console.log(`Skipped saving scroll position (back navigation)`);
    }
  });

  document.addEventListener("astro:after-swap", () => {
    scrollRestoration = true;
    const newPath = window.location.pathname;
    console.log(`After swap - New path: ${newPath}`);

    const scrollPositions = JSON.parse(sessionStorage.getItem("scrollPositions") || "{}");
    const savedScrollPosition = scrollPositions[newPath];
    console.log(`Retrieved saved scroll position for ${newPath}: ${savedScrollPosition}`);

    if (savedScrollPosition !== undefined) {
      // Delay scroll restoration
      setTimeout(() => {
        const element = document.elementFromPoint(0, savedScrollPosition);
        if (element) {
          element.scrollIntoView();
          console.log(`Scrolled to element at position: ${savedScrollPosition}`);
        } else {
          window.scrollTo(0, savedScrollPosition);
          console.log(`Scrolled to position: ${savedScrollPosition}`);
        }
        scrollRestoration = false;
      }, 700);
      if (!isBack) {
        delete scrollPositions[newPath];
        sessionStorage.setItem("scrollPositions", JSON.stringify(scrollPositions));
        console.log(`Cleared saved scroll position for ${newPath}`);
      }
    } else {
      console.log(`No saved scroll position found for ${newPath}`);
      scrollRestoration = false;
    }
  });

  document.addEventListener("astro:page-load", () => {
    console.log(`Page fully loaded: ${window.location.pathname}`);
  });

  // Prevent scroll events during transition
  window.addEventListener('scroll', (e) => {
    if (scrollRestoration) {
      e.preventDefault();
      window.scrollTo(0, 0);
    }
  }, { passive: false });
</script>

Here’s a breakdown of what the code does:

  1. Uses sessionStorage to store scroll positions for each page.
  2. Captures scroll position before navigation using astro:before-preparation event.
  3. Restores scroll position after page transition using astro:after-swap event.
  4. Implements a delay before scroll restoration for improved reliability.
  5. Handles both element-based and pixel-based scroll restoration.
  6. Clears saved scroll positions after restoration to maintain cleanliness.
  7. Prevents scroll events during transition to avoid visual glitches.
  8. Logs various steps for debugging purposes.