How to handle anchors (bookmarks) with Vue Router?

Hey guys,

I’m looking for a smart way to handle in-page anchors with Vue Router. Consider the following:

<router-link to="#app">Apply Now</router-link>
<!-- some HTML markup in between... -->
<div id="app">...</div>

The “scroll to anchor” behavior described in the docs works fine except:

  • When you click on the anchor, it brings you down to the div id="app". Now scroll away from the div back to the anchor and try clicking it again – this time you will not jump down to the div. In fact, the anchor will retain the class router-link-active and the URL will still contain the hash /#app;
  • With the steps above, if you refresh the page (the URL will still contain the hash) and click on the anchor, nothing will happen either.

This is very unfortunate from the UX perspective because a potential customer has to manually scroll all the way down again to reach the application section.

I was wondering if Vue Router covers this situation. For reference, here’s my router:

export default new VueRouter({
	routes,
	mode: 'history',
	scrollBehavior(to, from, savedPosition) {
		if (to.hash) {
			return { selector: to.hash }
		} else if (savedPosition) {
    		return savedPosition;
    	} else {
			return { x: 0, y: 0 }
		}
	}
})
2 Likes

You’re going to have to add additional logic. Because the hash hasn’t changed then the route hasn’t changed therefore nothing happens. What you could do is add logic to your anchored links that says “if the route hash matches the link hash, then trigger scrolling manually”

“if the route hash matches the link hash, then trigger scrolling manually”

I thought that this if-block

if (to.hash) { // if has a hash...
    return { selector: to.hash } // scroll to the element
}

did exactly that. Isn’t to.hash the same as window.location.hash?

That’s why I was confused by the behavior of Vue Router. Can you give me some guidance on what / where I have to change code?

Well, yes, it does technically do that, but only when a route has changed. That’s your issue, the route isn’t changing therefore the scrollBehaviour logic is never called. Off the top of my head, one solution could be to write a component that acts similarily to router-link but when the link is clicked it checks if the current route has the hash or not, if it does then scroll to the anchor otherwise push the route.

1 Like

For anyone stuck on this issue, there is a fairly manual work-around as I answered on Stackoverflow.

Utilize the $route.hash in the mounted hook of the component that holds your <router-view></router-view> to solve the issue of refreshes.

<script>
export default {
  name: 'app',
  mounted: function()
  {
    // From testing, without a brief timeout, it won't work.
    setTimeout(() => this.scrollFix(this.$route.hash), 1),
  },
  methods: {
    scrollFix: function(hashbang)
    {
      location.href = hashbang;
    }
  }
}
</script>

Then to solve the issue of second clicks not scrolling use the native modifier on your router-link.

<router-link to="#scroll" @click.native="scrollFix('#scroll')">Scroll</router-link>

There is probably a much better solution, but this will work.

1 Like

Your solutions works but it has a problem when navigating from another view that doesn’t have that anchors. Here is my complete solution for that problem:

<script>
const TIMEOUT = 1;

export default {
  name: 'NavLinks',
  mounted () {
    // From testing, without a brief timeout, it won't work.
    if (this.$route.hash) {
      setTimeout(() => this.scrollTo(this.$route.hash), TIMEOUT)
    }
  },
  methods: {
    scrollTo: function (hashtag) {
      setTimeout(() => { location.href = hashtag }, TIMEOUT)
    }
  }
}
</script>
1 Like

when rewriting someones code pls use the same variables kthxbye