Skip to content
ExponaBLOG
VUE.JS & NUXT · DAY 097 / 100

Scroll behavior in Vue Router for Vue & Nuxt developers

Natural scrolling between pages.

Expona Admin7 min read

Introduction

When users navigate between pages in a Vue or Nuxt application, they expect the browser to behave predictably—scrolling to the top on new pages, jumping to anchor links, or returning to their previous scroll position when using the back button. Without explicit configuration, Vue Router leaves this behavior up to the browser, which often results in jarring or inconsistent experiences. Scroll behavior in Vue Router for Vue & Nuxt developers is a powerful but frequently overlooked feature that lets us take precise control over the viewport, ensuring a polished, native-like navigation experience.

In this article, we'll walk through how to implement scroll behavior strategies that keep users oriented and engaged, from simple top-on-navigation patterns to restoring saved scroll positions and jumping to hash anchors. By the end, you'll have the patterns and practices we use in production applications across e-commerce and SaaS platforms.

ScrollBehavior in router config

The foundation of scroll control in Vue Router is the scrollBehavior function, defined in your router configuration. This function runs every time a navigation completes and returns an object describing where the browser should scroll.

In a standard Vue application, your router setup might look like this:

typescript
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', component: Home },
    { path: '/products', component: Products },
    { path: '/checkout', component: Checkout },
  ],
  scrollBehavior(to, from, savedPosition) {
    // Return an object with x and y coordinates
    // or the savedPosition from browser history
    if (savedPosition) {
      return savedPosition
    } else {
      return { left: 0, top: 0 }
    }
  },
})

export default router

For Nuxt projects, the configuration lives in nuxt.config.ts under the router property, or you can export a function from app/router.ts (Nuxt 3.8+):

typescript
export default defineNuxtConfig({
  router: {
    scrollBehavior(to, from, savedPosition) {
      if (savedPosition) {
        return savedPosition
      }
      return { left: 0, top: 0 }
    },
  },
})

The scrollBehavior function receives three parameters: to (the destination route), from (the source route), and savedPosition (coordinates stored by the browser when the user navigated away). The simplest implementation scrolls to the top for every new page unless the user is going back, in which case we restore their position.

This basic pattern works well for most applications. When a user clicks a link to /products, the page scrolls to the top, showing the product listing header. When they press back, they return to their previous scroll position rather than seeing the bottom of the previous page.

Restore saved positions on back

Modern browsers automatically track scroll positions in their history stack. When savedPosition is provided to scrollBehavior, it means the user is navigating backward (or forward in history), and we should restore them to where they were. This creates a seamless experience that mirrors native applications.

Here's a more refined implementation that handles various navigation patterns:

typescript
scrollBehavior(to, from, savedPosition) {
  // User is going back or forward in browser history
  if (savedPosition) {
    return savedPosition
  }

  // User is clicking a link on the page
  // Scroll to top for new pages, but not for query-only changes
  if (to.path !== from.path) {
    return { left: 0, top: 0 }
  }

  // Query string or hash changed but path is the same
  // (e.g., filter applied on product listing)
  // Don't scroll in this case
  return false
}

This pattern prevents jarring scrolls when a user refines a product search with filters or applies sorting. The page is the same, just with different query parameters, so we keep them in place. When they navigate to a genuinely different page, though, we reset to the top.

In real e-commerce applications, we've seen this pattern reduce support tickets about "confusing navigation." Users expect to see filters they just applied, not the top of the page again.

Scroll to hash anchors

Hash anchors—links pointing to specific elements like #features or #pricing—are common in landing pages and documentation. By default, Vue Router doesn't handle hash anchors automatically. We need to implement this in our scrollBehavior function by finding the element and scrolling to it.

Here's a production-ready implementation:

typescript
scrollBehavior(to, from, savedPosition) {
  // Browser back/forward button
  if (savedPosition) {
    return savedPosition
  }

  // Hash anchor link
  if (to.hash) {
    // Delay slightly to ensure DOM is rendered
    return new Promise((resolve) => {
      setTimeout(() => {
        const element = document.querySelector(to.hash)
        if (element) {
          resolve({
            el: element,
            behavior: 'smooth',
          })
        } else {
          resolve({ left: 0, top: 0 })
        }
      }, 100)
    })
  }

  // Regular navigation
  if (to.path !== from.path) {
    return { left: 0, top: 0 }
  }

  return false
}

The key here is using a Promise and a small delay. Vue Router's DOM rendering completes asynchronously, so we need to wait before querying for the target element. If the element exists, we return an object with the el property pointing to it. If it doesn't exist—perhaps due to a typo in the URL—we fall back to scrolling to the top.

For a SaaS dashboard with a settings page containing multiple sections, users might link directly to a specific section via /settings#notifications. This pattern ensures they land exactly where they need to be.

Smooth scrolling where appropriate

CSS has supported smooth scrolling for years, and Vue Router's scrollBehavior can leverage it. Smooth scrolling feels more polished than instant jumps, especially on landing pages or when scrolling to anchors within the same page.

To enable smooth scrolling globally, add this CSS:

typescript
/* In your global CSS or app.vue */
html {
  scroll-behavior: smooth;
}

However, smooth scrolling isn't always appropriate. On fast navigation between pages, an instant scroll to the top feels snappier. Here's a selective approach:

typescript
scrollBehavior(to, from, savedPosition) {
  if (savedPosition) {
    return savedPosition
  }

  if (to.hash) {
    return new Promise((resolve) => {
      setTimeout(() => {
        const element = document.querySelector(to.hash)
        if (element) {
          // Smooth scroll for hash anchors
          resolve({
            el: element,
            behavior: 'smooth',
          })
        } else {
          // Instant scroll for fallback
          resolve({ left: 0, top: 0 })
        }
      }, 100)
    })
  }

  if (to.path !== from.path) {
    // Instant scroll when changing pages
    return { left: 0, top: 0 }
  }

  return false
}

This approach uses smooth scrolling for anchor links (which the user explicitly triggered) and instant scrolling when changing pages (which feels responsive). You can also disable the CSS scroll-behavior property and control everything through Vue Router if you prefer consistent behavior across all scroll events.

Common mistakes

Not handling async components: If your routes use dynamic imports or async components, the DOM might not be ready when scrollBehavior runs. Always wrap hash anchor lookups in a Promise with a small delay, as shown in our examples.

Scrolling inside nested divs: The examples above scroll the window. If your layout uses a scrollable container (a modal, sidebar, or custom scroll area), you'll need to target that element instead. Store a reference to your scroll container and use element.scrollTop or similar approaches rather than relying on window scroll.

Ignoring query parameter changes: Filtering a product list by adding query parameters shouldn't scroll the page. Our pattern checks if to.path !== from.path to distinguish between genuine page changes and parameter-only updates.

Forgetting about SPA behavior: In a traditional multi-page site, the browser always scrolls to the top when loading a new HTML document. In a single-page app, you must implement this behavior yourself or users will land mid-page.

Key takeaways

  • The scrollBehavior function in your router config controls viewport positioning on every navigation.
  • Use savedPosition to restore scroll position when users navigate backward or forward in browser history.
  • Handle hash anchors with a Promise and DOM query; add a small delay to ensure the component has rendered.
  • Distinguish between path changes (scroll to top), query-only changes (stay in place), and back-button navigation (restore position).
  • Apply smooth scrolling selectively for polished UX—usually for explicit anchor links rather than page-to-page navigation.
  • In scrollable containers, target the container element directly rather than assuming window scroll behavior.
  • Test on real devices with different network speeds; async components may render slower, affecting hash anchor timing.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles