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:
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 routerFor 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+):
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:
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:
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:
/* 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:
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
scrollBehaviorfunction in your router config controls viewport positioning on every navigation. - Use
savedPositionto 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.






Comments
Loading comments…