Nuxt.js | How to retain Scroll Position when returning to page without Navigation History

The Nuxt framework already provides a feature to preserve the scroll position when returning to a page if the user clicks the browser’s back button (or we use $router.go(n) which triggers the same event: popstate navigation).

I was facing a situation that needed to return to a page with $router.replace instead of $router.go(-1) (I explain the reason for this need below). This requirement gave me a problem: $router.replace doesn’t trigger the popstate event, so keeping scroll position does not work, and the page always loads at to the top.

In this article I will detail this solution. What motivated me to write this article is that I didn’t find it on google, I hope it helps someone.

The Problem

Let me detail the problem. I have a page with lots of cards and a Sign In button so the user can access the details of those cards. Suppose you are browsing and want to login. After logging in you would like to return the same position you were in. But look at the default behavior of Nuxt:

After login, the user is redirected to the previous page at the top

After authenticating, the user returns to the previous page, but no longer in the position he was in. This behavior disrupts the user experience.

As I said at the beginning of this article, this problem happens because in my login.vue, I use $router.replace instead of $router.go(-1) to return the source page after login. You might be wondering, why don’t you use $router.go(-1) and solve your problem? Using $router.replace is necessary so that the user cannot return to the login screen with the browser’s back button. $router.go(-1) allows this.

The Solution

We will create a router middleware to store the scroll position of the pages that have the scrollPos meta property (we will create this too). And then change Nuxt’s scrollBehavior to when the scrollPos object exists in the route’s meta property, load the page at the position stored in the object.

1) Add meta in Nuxt Router

On the page where we want to preserve the scroll position, we must create the meta property with the following object:

(in my case, it’s the page that has the different cards, which I called search.vue)

//search.vue
export default {
//...
meta: {
scrollPos: {
x: 0,
y: 0
}
}

}

The logic will be as follows, every page that has this scrollPos meta property, we will store the scroll position in this object.

2) Creating the Router Middleware

We will create a router middleware to check if route has the scollPos meta property, if so, the scroll position will be store on this object when leaving the page.

In your Nuxt project, create a folder named middleware and inside the file adjustScroll.js:

Middleware Folder

Paste this code inside adjustScroll.js:

export default function({ from }) { 
const scrollPos = from?.meta[0].scrollPos
if (scrollPos && Object.keys(scrollPos).length > 0) {
scrollPos.y = window.scrollY || 0
scrollPos.x = window.scrollX || 0
}
}

Let’s understand… This function will be executed every time we navigate between pages. It checks if the scrollPos object exists within the Meta property’s array. If so, it stores the scroll position.

Include this middleware in your Nuxt settings (nuxt.config.js):

//nuxt.config.js
export default {
//...
router: {
middleware: ['adjustScroll']
},

}

Okay, we store the scroll position, now how do we scroll when we return to the page? Let’s see in the next topic.

3) Custom scrollBehavior after page loaded

Nuxt allows us to customize the scrolling behavior after page loaded. That’s what we’re going to do here, when there’s the scrollPos object in the meta property, we’re going to scroll the page to the values ​​stored in that object.

In your Nuxt project, create a folder named app and inside the file router.scrollBehavior.js :

App Folder

This file allows us to change the default scrolling behavior of Nuxt. As it doesn’t allow us to extend, the documentation recommendation is to change the default Nuxt code found here.

I made some additions to this code to adapt to our need and changed the position of the declaration of the nuxt variable. Copy and paste the code below into the file (router.scrollBehavior.js):

Your linter will complain about some of Nuxt’s own internal template syntax, you can ignore that.

//router.scrollBehavior.js
<% if (router.scrollBehavior) { %>
<%= isTest ? '/* eslint-disable quotes, semi, indent, comma-spacing, key-spacing, object-curly-spacing, space-before-function-paren */' : '' %>
export default <%= serializeFunction(router.scrollBehavior) %>
<%= isTest ? '/* eslint-enable quotes, semi, indent, comma-spacing, key-spacing, object-curly-spacing, space-before-function-paren */' : '' %>
<% } else { %>import { getMatchedComponents, setScrollRestoration } from './utils'
if (process.client) {
if ('scrollRestoration' in window.history) {
setScrollRestoration('manual')
// reset scrollRestoration to auto when leaving page, allowing page reload
// and back-navigation from other pages to use the browser to restore the
// scrolling position.
window.addEventListener('beforeunload', () => {
setScrollRestoration('auto')
})
// Setting scrollRestoration to manual again when returning to this page.
window.addEventListener('load', () => {
setScrollRestoration('manual')
})
}
}
function shouldScrollToTop(route) {
const Pages = getMatchedComponents(route)
if (Pages.length === 1) {
const { options = {} } = Pages[0]
return options.scrollToTop !== false
}
return Pages.some(({ options }) => options && options.scrollToTop)
}
export default function (to, from, savedPosition) {
// If the returned position is falsy or an empty object, will retain current scroll position
let position = false
const isRouteChanged = to !== from
const nuxt = window.<%= globals.nuxt %>
// savedPosition is only available for popstate navigations (back button)
const metaScrollPos = nuxt.context.route.meta[0]?.scrollPos
if (savedPosition || metaScrollPos) {
position = savedPosition || metaScrollPos
} else if (isRouteChanged && shouldScrollToTop(to)) {
position = { x: 0, y: 0 }
}
if (
// Initial load (vuejs/vue-router#3199)
!isRouteChanged ||
// Route hash changes
(to.path === from.path && to.hash !== from.hash)
) {
nuxt.$nextTick(() => nuxt.$emit('triggerScroll'))
}
return new Promise((resolve) => {
// wait for the out transition to complete (if necessary)
nuxt.$once('triggerScroll', () => {
// coords will be used if no selector is provided,
// or if the selector didn't match any element.
if (to.hash) {
let hash = to.hash
// CSS.escape() is not supported with IE and Edge.
if (typeof window.CSS !== 'undefined' && typeof window.CSS.escape !== 'undefined') {
hash = '#' + window.CSS.escape(hash.substr(1))
}
try {
const el = document.querySelector(hash)
if (el) {
// scroll to anchor by returning the selector
position = { selector: hash }
// Respect any scroll-margin-top set in CSS when scrolling to anchor
const y = Number(getComputedStyle(el)['scroll-margin-top']?.replace('px', ''))
if (y) {
position.offset = { y }
}
}
} catch (e) {
<%= isTest ? '// eslint-disable-next-line no-console' : '' %>
console.warn('Failed to save scroll position. Please add CSS.escape() polyfill (https://github.com/mathiasbynens/CSS.escape).')
}
}
resolve(position)
})
})
}
<% } %>

Below I highlight only the part I changed from the default template. Let’s understand.

First I changed the position of the nuxt variable declaration (in the default template it occurs below this conditional). This variable stores the Nuxt context and as we need to access it in the conditional below, I changed the position of the declaration.

The second change is in the conditional. We check if there is a scrollPos object inside the route’s meta property. If so, we pass the stored values ​​to the position variable (which determines the scrolling).

//..
const nuxt = window.<%= globals.nuxt %>
// savedPosition is only available for popstate navigations (back button)
const metaScrollPos = nuxt.context.route.meta[0]?.scrollPos
if (savedPosition || metaScrollPos) {
position = savedPosition || metaScrollPos
} else if (isRouteChanged && shouldScrollToTop(to)) {
position = { x: 0, y: 0 }
}
//...

Our solution is complete. Every page that has the scrollPos object in the meta property will store the scroll position for when we return. If at some point you want this page to load at to the top, just programmatically change the scrollPos object to { x: 0, y: 0}

Finally, our middleware running

Preserve scroll position when navigate between pages without navigation history

Now the user after logging in returns to the scroll position he was initially 🙂


Nuxt.js | How to retain Scroll Position when returning to page without Navigation History was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.

The Nuxt framework already provides a feature to preserve the scroll position when returning to a page if the user clicks the browser’s back button (or we use $router.go(n) which triggers the same event: popstate navigation).

I was facing a situation that needed to return to a page with $router.replace instead of $router.go(-1) (I explain the reason for this need below). This requirement gave me a problem: $router.replace doesn’t trigger the popstate event, so keeping scroll position does not work, and the page always loads at to the top.

In this article I will detail this solution. What motivated me to write this article is that I didn’t find it on google, I hope it helps someone.

The Problem

Let me detail the problem. I have a page with lots of cards and a Sign In button so the user can access the details of those cards. Suppose you are browsing and want to login. After logging in you would like to return the same position you were in. But look at the default behavior of Nuxt:

After login, the user is redirected to the previous page at the top

After authenticating, the user returns to the previous page, but no longer in the position he was in. This behavior disrupts the user experience.

As I said at the beginning of this article, this problem happens because in my login.vue, I use $router.replace instead of $router.go(-1) to return the source page after login. You might be wondering, why don’t you use $router.go(-1) and solve your problem? Using $router.replace is necessary so that the user cannot return to the login screen with the browser’s back button. $router.go(-1) allows this.

The Solution

We will create a router middleware to store the scroll position of the pages that have the scrollPos meta property (we will create this too). And then change Nuxt’s scrollBehavior to when the scrollPos object exists in the route’s meta property, load the page at the position stored in the object.

1) Add meta in Nuxt Router

On the page where we want to preserve the scroll position, we must create the meta property with the following object:

(in my case, it’s the page that has the different cards, which I called search.vue)

//search.vue
export default {
//...
meta: {
scrollPos: {
x: 0,
y: 0
}
}

}

The logic will be as follows, every page that has this scrollPos meta property, we will store the scroll position in this object.

2) Creating the Router Middleware

We will create a router middleware to check if route has the scollPos meta property, if so, the scroll position will be store on this object when leaving the page.

In your Nuxt project, create a folder named middleware and inside the file adjustScroll.js:

Middleware Folder

Paste this code inside adjustScroll.js:

export default function({ from }) { 
const scrollPos = from?.meta[0].scrollPos
if (scrollPos && Object.keys(scrollPos).length > 0) {
scrollPos.y = window.scrollY || 0
scrollPos.x = window.scrollX || 0
}
}

Let’s understand… This function will be executed every time we navigate between pages. It checks if the scrollPos object exists within the Meta property’s array. If so, it stores the scroll position.

Include this middleware in your Nuxt settings (nuxt.config.js):

//nuxt.config.js
export default {
//...
router: {
middleware: ['adjustScroll']
},

}

Okay, we store the scroll position, now how do we scroll when we return to the page? Let’s see in the next topic.

3) Custom scrollBehavior after page loaded

Nuxt allows us to customize the scrolling behavior after page loaded. That’s what we’re going to do here, when there’s the scrollPos object in the meta property, we’re going to scroll the page to the values ​​stored in that object.

In your Nuxt project, create a folder named app and inside the file router.scrollBehavior.js :

App Folder

This file allows us to change the default scrolling behavior of Nuxt. As it doesn’t allow us to extend, the documentation recommendation is to change the default Nuxt code found here.

I made some additions to this code to adapt to our need and changed the position of the declaration of the nuxt variable. Copy and paste the code below into the file (router.scrollBehavior.js):

Your linter will complain about some of Nuxt’s own internal template syntax, you can ignore that.

//router.scrollBehavior.js
<% if (router.scrollBehavior) { %>
<%= isTest ? '/* eslint-disable quotes, semi, indent, comma-spacing, key-spacing, object-curly-spacing, space-before-function-paren */' : '' %>
export default <%= serializeFunction(router.scrollBehavior) %>
<%= isTest ? '/* eslint-enable quotes, semi, indent, comma-spacing, key-spacing, object-curly-spacing, space-before-function-paren */' : '' %>
<% } else { %>import { getMatchedComponents, setScrollRestoration } from './utils'
if (process.client) {
if ('scrollRestoration' in window.history) {
setScrollRestoration('manual')
// reset scrollRestoration to auto when leaving page, allowing page reload
// and back-navigation from other pages to use the browser to restore the
// scrolling position.
window.addEventListener('beforeunload', () => {
setScrollRestoration('auto')
})
// Setting scrollRestoration to manual again when returning to this page.
window.addEventListener('load', () => {
setScrollRestoration('manual')
})
}
}
function shouldScrollToTop(route) {
const Pages = getMatchedComponents(route)
if (Pages.length === 1) {
const { options = {} } = Pages[0]
return options.scrollToTop !== false
}
return Pages.some(({ options }) => options && options.scrollToTop)
}
export default function (to, from, savedPosition) {
// If the returned position is falsy or an empty object, will retain current scroll position
let position = false
const isRouteChanged = to !== from
const nuxt = window.<%= globals.nuxt %>
// savedPosition is only available for popstate navigations (back button)
const metaScrollPos = nuxt.context.route.meta[0]?.scrollPos
if (savedPosition || metaScrollPos) {
position = savedPosition || metaScrollPos
} else if (isRouteChanged && shouldScrollToTop(to)) {
position = { x: 0, y: 0 }
}
if (
// Initial load (vuejs/vue-router#3199)
!isRouteChanged ||
// Route hash changes
(to.path === from.path && to.hash !== from.hash)
) {
nuxt.$nextTick(() => nuxt.$emit('triggerScroll'))
}
return new Promise((resolve) => {
// wait for the out transition to complete (if necessary)
nuxt.$once('triggerScroll', () => {
// coords will be used if no selector is provided,
// or if the selector didn't match any element.
if (to.hash) {
let hash = to.hash
// CSS.escape() is not supported with IE and Edge.
if (typeof window.CSS !== 'undefined' && typeof window.CSS.escape !== 'undefined') {
hash = '#' + window.CSS.escape(hash.substr(1))
}
try {
const el = document.querySelector(hash)
if (el) {
// scroll to anchor by returning the selector
position = { selector: hash }
// Respect any scroll-margin-top set in CSS when scrolling to anchor
const y = Number(getComputedStyle(el)['scroll-margin-top']?.replace('px', ''))
if (y) {
position.offset = { y }
}
}
} catch (e) {
<%= isTest ? '// eslint-disable-next-line no-console' : '' %>
console.warn('Failed to save scroll position. Please add CSS.escape() polyfill (https://github.com/mathiasbynens/CSS.escape).')
}
}
resolve(position)
})
})
}
<% } %>

Below I highlight only the part I changed from the default template. Let’s understand.

First I changed the position of the nuxt variable declaration (in the default template it occurs below this conditional). This variable stores the Nuxt context and as we need to access it in the conditional below, I changed the position of the declaration.

The second change is in the conditional. We check if there is a scrollPos object inside the route’s meta property. If so, we pass the stored values ​​to the position variable (which determines the scrolling).

//..
const nuxt = window.<%= globals.nuxt %>
// savedPosition is only available for popstate navigations (back button)
const metaScrollPos = nuxt.context.route.meta[0]?.scrollPos
if (savedPosition || metaScrollPos) {
position = savedPosition || metaScrollPos
} else if (isRouteChanged && shouldScrollToTop(to)) {
position = { x: 0, y: 0 }
}
//...

Our solution is complete. Every page that has the scrollPos object in the meta property will store the scroll position for when we return. If at some point you want this page to load at to the top, just programmatically change the scrollPos object to { x: 0, y: 0}

Finally, our middleware running

Preserve scroll position when navigate between pages without navigation history

Now the user after logging in returns to the scroll position he was initially 🙂


Nuxt.js | How to retain Scroll Position when returning to page without Navigation History was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.


Print Share Comment Cite Upload Translate
APA
Luiz Eduardo Zappa | Sciencx (2024-03-29T15:47:21+00:00) » Nuxt.js | How to retain Scroll Position when returning to page without Navigation History. Retrieved from https://www.scien.cx/2022/02/13/nuxt-js-how-to-retain-scroll-position-when-returning-to-page-without-navigation-history/.
MLA
" » Nuxt.js | How to retain Scroll Position when returning to page without Navigation History." Luiz Eduardo Zappa | Sciencx - Sunday February 13, 2022, https://www.scien.cx/2022/02/13/nuxt-js-how-to-retain-scroll-position-when-returning-to-page-without-navigation-history/
HARVARD
Luiz Eduardo Zappa | Sciencx Sunday February 13, 2022 » Nuxt.js | How to retain Scroll Position when returning to page without Navigation History., viewed 2024-03-29T15:47:21+00:00,<https://www.scien.cx/2022/02/13/nuxt-js-how-to-retain-scroll-position-when-returning-to-page-without-navigation-history/>
VANCOUVER
Luiz Eduardo Zappa | Sciencx - » Nuxt.js | How to retain Scroll Position when returning to page without Navigation History. [Internet]. [Accessed 2024-03-29T15:47:21+00:00]. Available from: https://www.scien.cx/2022/02/13/nuxt-js-how-to-retain-scroll-position-when-returning-to-page-without-navigation-history/
CHICAGO
" » Nuxt.js | How to retain Scroll Position when returning to page without Navigation History." Luiz Eduardo Zappa | Sciencx - Accessed 2024-03-29T15:47:21+00:00. https://www.scien.cx/2022/02/13/nuxt-js-how-to-retain-scroll-position-when-returning-to-page-without-navigation-history/
IEEE
" » Nuxt.js | How to retain Scroll Position when returning to page without Navigation History." Luiz Eduardo Zappa | Sciencx [Online]. Available: https://www.scien.cx/2022/02/13/nuxt-js-how-to-retain-scroll-position-when-returning-to-page-without-navigation-history/. [Accessed: 2024-03-29T15:47:21+00:00]
rf:citation
» Nuxt.js | How to retain Scroll Position when returning to page without Navigation History | Luiz Eduardo Zappa | Sciencx | https://www.scien.cx/2022/02/13/nuxt-js-how-to-retain-scroll-position-when-returning-to-page-without-navigation-history/ | 2024-03-29T15:47:21+00:00
https://github.com/addpipe/simple-recorderjs-demo