Rethinking Route Navigation
Vue Router ships with a handful of features that go beyond the basics of dynamic matching, named routes, and programmatic navigation. Some of these extras — scroll control, data fetching strategies, transitions, and route guards — can meaningfully improve the UX of a Vue application. Below we walk through each with a small demo app that fetches posts from JSONPlaceholder.
Controlling Scroll Position
By default, Vue Router preserves the page scroll position when you navigate between routes. That means if you click a link near the bottom of a long list, the next page will render at that same vertical offset. That behavior is often jarring on a new page where a user expects to start at the top.
To demonstrate, we can build an app that lists posts and shows individual post detail pages. After installing Axios for HTTP requests, the home page fetches posts from JSONPlaceholder and renders them as links to detail routes built from each post's id.
# using YARN
yarn add axios
# or NPM
npm install axios
The detail page (Post.vue) receives the route parameters as props. This avoids the need to reach into $route.params inside the component and keeps the component more reusable and testable.
<template>
<div class="about">
<div class="post">
<h1>{{ post.title }}</h1>
<p v-html="post.body"></p>
</div>
<p>End of page</p>
</div>
</template>
<script>
export default {
name: "Post",
props: ["id", "post"],
};
</script>
<style>
.post {
padding: 0 30px;
height: 110vh;
margin: 0 auto;
}
p {
margin: 10px 0;
}
</style>
To make the default scroll behavior noticeable, the detail page includes styling that extends its height beyond the viewport (using a height of 110vh). If you scroll down on the home page and click one of the later posts, the detail page will land halfway down the page — not at the top.
The fix is straightforward: add a scrollBehavior function to the router instance. Returning { top: 0 } ensures every navigation lands at the top of the new page unless you explicitly override it.
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
Vue.use(VueRouter)
const routes = [...]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes,
//add this
scrollBehavior(to, from, savedPosition) {
return { x: 0, y: 0 }
}
})
export default router
Fetching Data Before Navigation
Most Vue components fetch data inside the mounted or created hook. Vue Router offers another pattern: use the beforeRouteEnter guard within a component to fetch data before the route is confirmed. At that point, this is not yet available, but the guard receives vm (the component instance) in its callback, which you can use to assign data that will be rendered on the incoming view.
beforeRouteEnter(to, from, next) {
axios
.get("https://jsonplaceholder.typicode.com/posts")
.then((res) => {
next((vm) => vm.fetchData(res));
})
.catch((err) => {
console.error(err);
});
},
methods: {
fetchData(res) {
let post = res.data;
this.posts = post;
},
},
In this pattern, the navigation waits for the API response — and the page doesn’t flash a blank state while the data loads.
Route Transition Options
Vue’s <transition> component works for any element or component, including routed views. Wrapping <router-view> with a named transition applies CSS-based slide and fade effects to all route changes in the app.
<template>
<div id="app">
<div id="nav">
<router-link to="/">Home</router-link>
</div>
<transition name="slide-fade">
<router-view />
</transition>
</div>
</template>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
#nav {
padding: 30px;
}
#nav a {
font-weight: bold;
color: #2c3e50;
}
#nav a.router-link-exact-active {
color: #42b983;
}
.slide-fade-enter-active {
transition: transform 0.3s cubic-bezier(1, 0.5, 0.8, 1),
color 0.5s cubic-bezier(1, 0.5, 0.8, 1);
}
.slide-fade-leave-active {
transition: transform 1s cubic-bezier(1, 0.5, 0.8, 1),
color 1s cubic-bezier(1, 0.5, 0.8, 1);
}
.slide-fade-enter {
color: mediumblue;
transform: translateY(20px);
}
.slide-fade-leave-to {
transform: translateX(100px);
color: cyan;
}
</style>
You have three levels of control for route transitions:
- Per-route transitions: Add a
<transition>inside an individual routed component. This affects only navigation to and from that page — handy for giving a special treatment to, say, a landing or checkout page. - Dynamic transition names: Bind a
nameprop on the<transition>and compute it from the current route. For example, you can watch$routeand alternate between transition types depending on the postid(odd or even). - Global transitions: One transition rule that wraps all routed views uniformly.
Meta Fields and Route Guard Placement
Meta fields let you attach custom metadata to a route definition — for instance, whether a route requires authentication. These fields are accessible on the $route object. The field name is arbitrary (e.g. requiresAuth) and does nothing by itself until you combine it with a navigation guard.
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
Vue.use(VueRouter)
const routes = [{
path: '/',
name: 'Home',
component: Home,
// add meta to this route
meta: {
requiresAuth: true
}
},
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
There are three places to define navigation guards:
In-Component Guards
You can put a router guard directly inside a component. For example, in Home.vue, beforeRouteEnter can check an authentication value and toggle some UI that is only rendered for guests.
<template>
<div class="home">
<p v-if="loading" class="post--empty">Loading....</p>
<ol v-else>
<!-- add this text to your template -->
<p v-if="guest">Hi Guest</p>
<li v-for="post in posts" :key="post.id">
<router-link
:to="{ name: 'Post', params: { id: post.id, post: post } }"
>
{{ post.title }}
</router-link>
</li>
</ol>
</div>
</template>
<script>
// @ is an alias to /src
import axios from "axios";
export default {
name: "Home",
data() {
return {
posts: null,
// add this property
guest: false,
loading: false,
};
},
// add this function
beforeRouteEnter(to, from, next) {
if (to.matched.some((record) => record.meta.requiresAuth)) {
// this route requires auth, check if logged in
// if not, display guest greeting.
const loggedIn = JSON.parse(localStorage.getItem("loggedIn"));
if (!loggedIn) {
next((vm) => {
vm.guest = true;
});
} else {
next();
}
} else {
next(); // make sure to always call next()!
}
},
methods: {...}
};
</script>
<style>...</style>
That works together with code in App.vue that simulates an authentication state — in this case an isAuthenticated flag set in mounted. If the guard detects a guest, the message in Home.vue becomes visible.
export default {
mounted() {
localStorage.setItem("loggedIn", false);
}
};
Per-Route Guards
Attach beforeEnter directly inside a route definition in the router config. It fires when that route is about to be navigated to, and the logic can be more than console logging — for instance, redirecting or aborting navigation.
{
path: '/',
name: 'Home',
component: Home,
// add meta to this route
meta: {
requiresAuth: true
},
beforeEnter: (to, from, next) => {
if (to.name !== 'Home') {
console.log('Per-Route navigation guard ti wa online');
next()
} else next()
}
}
Global Guards
Global guards live on the router instance and run on every navigation. They are useful when you want a single enforcement point for authentication across multiple routes that share a meta flag.
All routes that should be protected get the requiresAuth meta field. A page like guest.vue is set up to display a login prompt. Then a global beforeEach guard checks the meta field on the destination route and redirects unauthenticated users (based on a value stored in localStorage) to that login page.
router.beforeEach((to, from, next) => {
if (to.matched.some((record) => record.meta.requiresAuth)) {
// this route requires auth, check if logged in
// if not, display guest greeting.
const loggedIn = JSON.parse(localStorage.getItem("loggedIn"));
if (!loggedIn) {
next({
path: '/login'
});
} else {
next();
}
} else {
next(); // make sure to always call next()!
}
})
With that guard in place, trying to visit any protected route automatically bounces to the login view. The guard ensures both the redirect and the enforcement of your meta rules happen consistently for the whole app.
What Router Configuration Can Really Do
Vue Router is often treated as little more than a URL-to-component mapper, but its API reaches far beyond basic route declarations. The features covered here — scroll control, route-level transitions, pre-mount data fetching, route metadata, and navigation guards — turn the router into a central coordination point for page behavior. Each one is configured declaratively on the route object, so the logic stays close to the route definition rather than scattered across component lifecycle hooks.
Guarding Navigation
Router guards are the recommended way to enforce access rules, validate params, or run async checks before a navigation is confirmed. The beforeEach guard runs globally on every route change, while beforeEnter on a specific route, and beforeRouteEnter, beforeRouteUpdate, and beforeRouteLeave inside a component give finer control over the timing of checks. They handle both the "can the user go there?" question and side effects like fetching data or redirecting unauthenticated visitors.
Reading Route Metadata
Static data attached to a route — such as a meta flag for whether a page requires auth, or a title for the document — is accessible through route.meta in any component or guard. Combining this with a guard is a standard pattern: a global beforeEach checks to.meta.requiresAuth and redirects to a login route when the user is not authenticated. Meta fields also serve as a single source of truth for per-page configuration, so template logic stays free of route-string comparisons.
Fetching Before Mount
Waiting for an API call inside a component's mounted hook leaves the view blank or partially rendered. Vue Router offers two cleaner approaches. The first fetches in a global guard: call the API, store the result (e.g., in a store), then call next(); the component reads the already-populated data during render. The alternative — the beforeRouteEnter guard — can receive data via a callback passed to next, which is invoked only after the component instance is created. This keeps the component itself from ever seeing a loading state.
Smooth Transitions Per Route
To animate route changes, wrap RouterView in a <transition> and give each route layer a name from its own meta — e.g., transition: 'fade' or transition: 'slide'. Then bind the transition's name attribute to the current route's meta value, and Vue will pick the corresponding CSS enter/leave classes automatically. Because each route declares its own effect, the same wrapper handles different animations without per-component logic.
Scroll Restoration and Positioning
The optional scrollBehavior function in the router options receives the to and from route objects plus the saved position (when using the browser's back/forward history). Returning an object like { top: 0 } resets scroll on navigation, while returning savedPosition restores the exact spot on a back traversal. Asynchronous handling also works: return a Promise that resolves to a position after, say, an element becomes ready. For specific routes, returning a selector (e.g., { selector: '#main' }) scrolls to an anchor or element. Returning {} or false leaves scroll untouched.
Assembling the Patterns
These features compound naturally. A typical production setup might combine a global guard for auth and route meta, a per-route transition for brand-consistent page changes, a data-fetch step before the view mounts, and scroll restoration so the back button feels native. The result is that navigation logic — not just routing — lives in one declarative place.
Resources
Related Reading
- Color Mechanics in UI Kits
- How to Deal With Big Tooling Upgrades in Large Organizations
- Regexes Got Good: The History and Future of Regular Expressions in JavaScript
- Generating Unique Random Numbers in JavaScript Using Sets




