14

I am trying to make a scrolling to anchor by means of scrollBehaviour in VueJS.

Generally, I change current router with the following way :

this.$router.push({path : 'componentName', name: 'componentName', hash: "#" + this.jumpToSearchField})

My VueRouter is defined as :

const router = new VueRouter({
  routes: routes,
  base: '/base/',
  mode: 'history',
  scrollBehavior: function(to, from, savedPosition) {
    let position = {}
    if (to.hash) {
      position = {
        selector : to.hash
      };
    } else {
      position = {x : 0 , y : 0}
    }
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(position)
      }, 10)
    })
  }
});

My routes :

[
  {
    path: '/settings/:settingsId',
    component: Settings,
    children: [
      {
        path: '',
        name: 'general',
        components: {
          default: General,
          summary: Summary
        }
      },
      {
        path: 'tab1',
        name: 'tab1',
        components: {
          default: tab1,
          summary: Summary
        }
      },
      {
        path: 'tab2',
        name: 'tab2',
        components: {
          default: tab2,
          summary: Summary
        }
      },
      {
        path: 'tab3',
        name: 'tab3',
        components: {
          default: tab3,
          summary: Summary
        }
      }
    ]
  },
  {
    path: '/*',
    component: Invalid
  }
];

Let's say I am on tab1 component and I would like to jump to anchor 'test' on tab3 component

After router.push() I see that scrollBehavior is trigged and component switches from tab1 to tab3 as well as URL is changed (e.g. http://localhost:8080/tab1 to http://localhost:8080/tab3#test) but windows location is not placed where anchor is but just on the top of the window.

And of course, I have textarea with id="test" on tab3 component

What can be wrong ?

Penny Liu
  • 15,447
  • 5
  • 79
  • 98
Yuriy Korovko
  • 515
  • 2
  • 9
  • 21
  • 1
    My initial thought is that when the route is resolved the DOM is not available to scroll to. Have you tried writing a small function to scrollto the element in the url hash on `mounted` – jonnycraze Feb 05 '19 at 19:54
  • I had this issue as well, ended up writing my own functions for it to jump, since I loaded content async so the jump needed to accure after it had loaded the async data, VueJS tries to jump before its loaded. – Anuga Feb 06 '19 at 16:08
  • @Anuga did you write your own func in scrollBehaviour ? – Yuriy Korovko Feb 07 '19 at 10:35
  • 1
    Nah, I combined jQuery scrollTo and some own code, in the component that needed the scrolling. After the "mount" it checks the path and hash and jumps when the page is done loaded. Not a bright solution, but it works flawlessly. – Anuga Feb 11 '19 at 07:32

16 Answers16

19

Use {left: 0, top: 0} instead of {x: 0, y: 0} and it will work.

I think it's a mistake in Vue documentation because if you log savedPosition on the console, you'll see {left: 0, top: 0} and when you change {x: 0, y: 0} like that everything will work perfectly.

EDIT 3/8/2022:

Now everything is fine with the documentation.

Farnaam Samadi
  • 190
  • 1
  • 6
  • Please provide a link to the documentation where you found this. The help page I found says `x:` and `y:` are correct. The error is somewhere else. https://router.vuejs.org/guide/advanced/scroll-behavior.html – Peter Krebs Feb 19 '21 at 15:18
  • 1
    Actually when I found `{x:0,y:0}` doesn't work properly, I logged savedPosition on the console to see what is the vue default format and simply it was `{left: 0, top: 0}` so I changed my code like that and everything was fine. – Farnaam Samadi Feb 21 '21 at 17:34
  • Okay nice find. Though what I'm saying is: A one-liner is not an answer you should give here in StackOverflow. Please add to your answer to help people understand why. Adding your anecdote could be a start but assuming the official documentation lies is a bit of a stretch. – Peter Krebs Feb 25 '21 at 09:47
  • 2
    This deserves a bajillion up-votes. Thank you so much. How has their documentation been wrong for two years now? – Rich Mar 17 '21 at 22:22
  • left and not not working for me either... going with Sweet Chilly Philly's hack , saved the day for me. Would love to resolve the mystery though – Sharkfin Apr 23 '21 at 08:57
  • 3
    Everything is fine with the documentation, in the documentation for vue-router 3 is indicated `{x:0,y:0}`, and for the vue-router 4 it is indicated `{left: 0, top: 0}`. you are all just using different versions of the vue-router here. – Xth Nov 02 '21 at 16:12
13

I couldn't get any of the other solutions around this working, and it was really frustrating.

What ended up working for me was the below:

const router = new Router({
    mode: 'history',
    routes: [...],
    scrollBehavior() {
        document.getElementById('app').scrollIntoView();
    }
})

I mount my VueJs app to #app so i can be certain it is present its available for selection.

Sweet Chilly Philly
  • 3,014
  • 2
  • 27
  • 37
10

this works for me in Vue 3 :

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes,
  scrollBehavior(to, from, SavedPosition) {
    if (to.hash) {
      const el = window.location.href.split("#")[1];
      if (el.length) {
        document.getElementById(el).scrollIntoView({ behavior: "smooth" });
      }
    } else if (SavedPosition) {
      return SavedPosition;
    } else {
      document.getElementById("app").scrollIntoView({ behavior: "smooth" });
    }
  },
});
4

Alright so I'm a bit late to the party but recently stumbled upon a fairly similar problem. I couldn't make my scrollBehavior work with the anchor. I finally found the root cause: my <router-view> was wrapped in a <transition>, which delayed the render/mounting of the anchor, like so:

<Transition name="fade-transition" mode="out-in">
  <RouterView />
</Transition>

What happened was:

  • You click on your redirect link with anchor
  • Router gets the info and changes the URL
  • <router-view> transition start. New content NOT YET mounted
  • scrollBehavior happens at the same time. The anchor is not found, so no scrolling
  • Transition is over, <router-view> correctly mounted/rendered

Without transition, the scrollBehavior return {selector: to.hash} works fine, since the content is instantly mounted, and the anchor exists in the page.

Because I did not want to remove the transition, I crafted a workaround which periodically tries to get the anchor element, and scrolls to it once it's rendered/found. It looks like this:

function wait(duration) {
  return new Promise((resolve) => setTimeout(resolve, duration));
}

async function tryScrollToAnchor(hash, timeout = 1000, delay = 100) {
  while (timeout > 0) {
    const el = document.querySelector(hash);
    if (el) {
      el.scrollIntoView({ behavior: "smooth" });
      break;
    }
    await wait(delay);
    timeout = timeout - delay;
  }
}

scrollBehavior(to, from, savedPosition) {
  if (to.hash) {
    // Required because our <RouterView> is wrapped in a <Transition>
    // So elements are mounted after a delay
    tryScrollToAnchor(to.hash, 1000, 100);
  } else if (savedPosition) {
    return savedPosition;
  } else {
    return { x: 0, y: 0 };
  }
}

Jordan Kowal
  • 1,444
  • 8
  • 18
2

I'm sharing my 2 cents on this problem for anyone like me looking for a working solution. Picking up on Sweet Chilly Philly, answer which was the only thing that worked for me, I'm adding the relevant code to make the URL hash work aswell:

  scrollBehavior: (to, from, savedPosition) => {
    if (to.hash) {
      Vue.nextTick(() => {
        document.getElementById(to.hash.substring(1)).scrollIntoView();
      })
      //Does not work but it's the vue way
      return {selector: to.hash}
    }

    if (savedPosition) {
      //Did not test this but maybe it also does not work
      return savedPosition
    }

    document.getElementById('app').scrollIntoView();
    //Does not work but it's the vue way
    return {x: 0, y: 0}
  }

I won't get into much detail about Vue.nextTick (you can read more about it here) but it kinda runs the code after the next DOM update, when the route already changed and the element referenced by the hash is already ready and can be reached through document.getElementById().

Bruno Tavares
  • 450
  • 1
  • 4
  • 18
2

As @Xth pointed out, a lot of the confusion around this topic comes from the fact that version 3 and 4 of vue-router handle the scrollBehaviour parameters differently. Here are both ways.

vue-router 4

scrollBehavior (to, from, savedPosition) {
    if (to.hash) {
      return {
        // x, y are replaced with left/top to define position, but when used with an element selector (el) will be used as offset
        el: to.hash,
        // offset has to be set as left and top at the top level
        left: 0,
        top: 64
      }
    }
  }

Official documentation V4: https://router.vuejs.org/guide/advanced/scroll-behavior.html

vue-router 3

scrollBehavior (to, from, savedPosition) {
    if (to.hash) {
      return {
        // x, y as top-level variables define position not offset
        selector: to.hash,
        // offset has to be set as an extra object
        offset: { x: 0, y: 64 }
      }
    }
  }

Official documentation V3: https://v3.router.vuejs.org/guide/advanced/scroll-behavior.html

staffan
  • 5,641
  • 3
  • 32
  • 28
Lars Schellhas
  • 309
  • 2
  • 9
1

I had a similar problem which was caused by following some example I found online. The problem in my case was that the item was not yet rendered. I was going off the after-leave event of a transition and though it threw no errors, it wasn't scrolling to the element. I changed it to the enter event of the transition and it works now.

I know the question didn't mention transitions, so maybe in this case you could try nextTick rather than setTimeout to make sure the element has rendered.

tony19
  • 125,647
  • 18
  • 229
  • 307
Dave
  • 1,409
  • 2
  • 18
  • 14
0

None of the above suggestions worked for me:

What I found and it works perfectly for my case is this:

App.vue

 <transition @before-enter="scrollTop" mode="out-in" appear>
   <router-view></router-view>
 </transition>

 methods: {
  scrollTop(){
    document.getElementById('app').scrollIntoView();
  },
}
LastM4N
  • 1,890
  • 1
  • 16
  • 22
0

If the default scroll to view does not work, you can achieve the same result with this:

// src/rouer/index.js


[ //routes 
{
  path: '/name',
  name: 'Name',
  component: () => import('../component')
},
.
.
.
]

 createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes,
  scrollBehavior (to, from, SavedPosition) {
    if (to.hash) {
      const el = window.location.href.split('#')[1]
      if (el.length) {
        document.getElementById(el).scrollIntoView({ behavior: 'smooth' })
      }
    } else if (SavedPosition) {
      return SavedPosition
    } else {
      document.getElementById('app').scrollIntoView({ behavior: 'smooth' })
    }
  }
})
Gblend
  • 11
  • 3
0

Why are you returning a promise?
The documentation just returns the position: https://router.vuejs.org/guide/advanced/scroll-behavior.html

So this should be instead:

  scrollBehavior: function(to, from, savedPosition) {
    let position = {}
    if (to.hash) {
      position = {
        selector : to.hash
      };
    } else {
      position = {x : 0 , y : 0}
    }
    return position;
  }

I haven't debugged if to.hash works as you intended, but the function call itself seems incorrect here.

Peter Krebs
  • 3,831
  • 2
  • 15
  • 29
0

For anyone else that is having this issue, I found removing overflow-x-hidden on the primary container solves the issue.

typefox09
  • 71
  • 7
0

Thanks to someone in another question , I decided to try the "delaying scroll" described here https://router.vuejs.org/guide/advanced/scroll-behavior.html#delaying-the-scroll

and it's working ! I copied it from the documentation, the code :

const router = createRouter({
  scrollBehavior(to, from, savedPosition) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve({ left: 0, top: 0, behavior: 'smooth' })
      }, 500)
    })
  },
})

Actually my problem was that when I used the syntax "return {}" it was working, but on mobile, it was not working at all. So I tried this, it works. And SMOOTH can be added !

Instead of writing 500 (ms), just write 0 if you want it to do it directly and it works fine. Finally something that works

ufa9
  • 29
  • 7
0

A little improvement on what @Masoud Ehteshami wrote, I implemented TypeScript and timeout

scrollBehavior(to, from, savedPosition) {
if (to.hash) {
  const el = window.location.href.split("#")[1];
  if (el.length) {
    setTimeout(() => {
      document.getElementById(el)?.scrollIntoView({
        behavior: "smooth",
      });
    }, 100);
  }
} else if (savedPosition) {
  return savedPosition;
} else {
  document
    .getElementById("app")
    ?.scrollIntoView({ behavior: "smooth" });
} }
0

I was having issues with this because of my layout. For me it wasn't the page or app container, or any container up in that root area. It was an inner page div that's height was calculated to fill the viewport, so the scrollbar was in that container.

I solved it by using (on that inner page div) CSS attribute:

scroll-padding-top: 99999px;

https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-padding-top

Usage of the large number causes the scrollbar to always be at the top of the div. My problem was the scrollbar was scrolled halfway down every time you reload the page, so this fixes it.

I read something that indicated if you need to update the padding value after loading, you may encounter issues, but that doesn't occur in my app.

There is also scroll-margin-top which could be similarly useful.

[edit]: I found the true problem to my issue. I noticed that scroll-padding-top caused the inner-page to scroll back to the top when I clicked on a checkbox near the bottom, so the scroll-adding-top solution was not-viable in my use case.

I noticed that the problem occurred on one of my pages but not another, and a key difference was that one was calling an el.focus() to focus an input element on page-load. Removing that got rid of the "being scrolled down" on page-load issue.

After more analysis, I fixed it by adding the preventScroll option to el.focus():

document.querySelector('#some-input').focus({ preventScroll: true });
agm1984
  • 15,500
  • 6
  • 89
  • 113
-1

Check out vue-routers support for this feature:

https://router.vuejs.org/guide/advanced/scroll-behavior.html

scrollBehavior (to, from, savedPosition) {
  if (to.hash) {
    return {
      selector: to.hash
      // , offset: { x: 0, y: 10 }
    }
  }
}
jonnycraze
  • 488
  • 3
  • 17
  • thank you for the answer. I tried your solution and it didn't help. Also, in mounted block I tried to log document.getElementById(idToScrollTo) and it returns 'null'. What the hell ? Mounted means that DOM has to exist. – Yuriy Korovko Feb 06 '19 at 09:55
  • Also, I've found that scroll works only with this.$refs.hash.$scrollIntoView() . But I cannot place offset or something else. idk what the hell and why standard functions as scrollBehaviour doesn't work :( – Yuriy Korovko Feb 06 '19 at 12:51
  • I was thinking more on this and digging around - are you using vue-router? If so they do support this functionality. – jonnycraze Feb 06 '19 at 15:44
  • Yeah, as you can see in the question's post, there is VueRouter and scrollBehaviour function is defined . The problem is scrollBehaviour doesn't work – Yuriy Korovko Feb 07 '19 at 09:44
-3

This can work too if jQuery is available:

scrollBehavior (to, from, savedPosition) {    
  $('#selector-in-element-with-scroll').scrollTop(0)
}
Ryan Charmley
  • 1,127
  • 15
  • 18