Client-side routing in Vue relies on mapping URL paths to component trees. A route record defines one such mapping—a single path paired with a single component. A collection of these records forms the route table, which the router instance consumes to decide which components to mount or destroy as the location changes.
In the template, <router-link> renders an accessible anchor for navigation. Its to attribute declares the destination path:
<router-link to="/dashboard">Dashboard</router-link>
The companion element <router-view> serves as a layout placeholder; the router will render the matched component exactly where this tag appears.
Configuring Routes
A route table is an array of record objects. Each record needs at least a path and a component:
const routeTable = [
{ path: '/dashboard', component: DashboardView },
{ path: '/profile', component: ProfileView }
]
Create the router manager by passing the table to the VueRouter constructor:
const appRouter = new VueRouter({
routes: routeTable
})
Finally, inject the router into the root Vue instance so the entire application can react to navigation events:
new Vue({
router: appRouter,
render: h => h(AppShell)
}).$mount('#app')
When a user clicks a <router-link>, Vue Router searches the table for a record whose path matches the to value. The associated component is then instantiated inside the nearest <router-view>.
Walkthrough: Two-View Application
Start with two simple view components. DashboardView.vue:
<template>
<div class="panel">
<h2>Dashboard</h2>
<p>{{ headline }}</p>
</div>
</template>
<script>
export default {
data() {
return {
headline: 'Welcome to the main dashboard'
}
}
}
</script>
ProfileView.vue:
<template>
<div class="panel">
<h2>Profile</h2>
<p>{{ description }}</p>
</div>
</template>
<script>
export default {
data() {
return {
description: 'Manage your personal information here'
}
}
}
</script>
In App.vue, provide navigation targets and a render outlet:
<template>
<div id="app">
<nav>
<router-link to="/dashboard">Dashboard</router-link>
<router-link to="/profile">Profile</router-link>
</nav>
<main>
<router-view />
</main>
</div>
</template>
Assemble the mapping in router.js:
import Vue from 'vue'
import VueRouter from 'vue-router'
import DashboardView from './DashboardView.vue'
import ProfileView from './ProfileView.vue'
Vue.use(VueRouter)
const routeTable = [
{ path: '/dashboard', component: DashboardView },
{ path: '/profile', component: ProfileView }
]
export default new VueRouter({ routes: routeTable })
And wire it into main.js:
import Vue from 'vue'
import AppShell from './App.vue'
import appRouter from './router'
new Vue({
el: '#app',
router: appRouter,
render: h => h(AppShell)
})
Redirecting the Root Path
Landing on / initially produces an empty outlet because no record matches the root path. A redirect resolves this by sending the browser to a valid record immediately:
const routeTable = [
{ path: '/dashboard', component: DashboardView },
{ path: '/profile', component: ProfileView },
{ path: '/', redirect: '/dashboard' }
]
Active Link Styling
Vue Router adds a default class—router-link-active—to the anchor that matches the current route. Target it with CSS:
a.router-link-active {
color: crimson;
}
Inactive links can be styled by attaching an ordinary class directly to the <router-link> element.
Dynamic Segments
Static paths are not always sufficient. Consider a TaskDetail view whose content depends on an identifier. Declare a dynamic segment with a colon:
{ path: '/task/:taskId', component: TaskDetail }
Links pass concrete values:
<router-link to="/task/501">Task 501</router-link>
<router-link to="/task/937">Task 937</router-link>
Inside TaskDetail.vue, expose the parameter through a computed property:
<template>
<div>
<h3>Task Details</h3>
<p>Currently viewing task {{ activeTaskId }}</p>
</div>
</template>
<script>
export default {
computed: {
activeTaskId() {
return this.$route.params.taskId
}
}
}
</script>
Because Vue reuses the same component instance when only the parameter changes, lifecycle hooks do not refire. To react to swaps, watch $route:
export default {
data() {
return {
currentId: null
}
},
watch: {
$route(updatedRoute) {
this.currentId = updatedRoute.params.taskId
}
}
}
Nested Routes
When a view hosts its own sub-navigation, use nested routes. Suppose Workspace contains three tabs: Projects, Reports, and Settings.
Workspace.vue:
<template>
<div>
<h2>Workspace</h2>
<nav>
<router-link to="/workspace/projects">Projects</router-link>
<router-link to="/workspace/reports">Reports</router-link>
<router-link to="/workspace/settings">Settings</router-link>
</nav>
<router-view />
</div>
</template>
The route configuration uses the children array:
const routeTable = [
{
path: '/workspace',
component: Workspace,
children: [
{ path: 'projects', component: ProjectsTab },
{ path: 'reports', component: ReportsTab },
{ path: 'settings', component: SettingsTab },
{ path: '', component: ProjectsTab }
]
}
]
The empty child path renders a default sub-view when the parent path is matched exactly.
Named Routes
Assigning a name to a record decouples templates from literal paths:
{
path: '/task/:taskId',
name: 'taskDetail',
component: TaskDetail
}
Reference it with an object in the to prop:
<router-link :to="{ name: 'taskDetail', params: { taskId: 501 } }">
Open Task 501
</router-link>
Programmatic Navigation
Imperative routing is available through the injected router instance:
this.$router.push('/workspace/projects')
Location objects are also accepted:
this.$router.push({ name: 'taskDetail', params: { taskId: 937 } })
Hash Mode vs. History Mode
Vue Router supports two strategies for updating the URL without requesting a new page from the server. Configure the desired behavior through the mode option:
const appRouter = new VueRouter({
mode: 'history', // defaults to 'hash' when omitted
routes: routeTable
})
Hash mode inserts a # fragment between the host and the application path, producing URLs such as https://app.com/#/dashboard. The fragment identifier is never sent to the server, which means static hosts and legacy servers require no additional configuration to support a single-page application.
History mode removes the hash by leveraging the HTML5 History API—pushState and replaceState—to generate clean URLs like https://app.com/dashboard. This approach requires server-side collaboration: when a visitor refreshes the browser or navigates directly to a deep link, the server must return the same index.html shell for every path that the application handles. Otherwise, the server responds with a 404 error because no physical file exists at /dashboard.
Comparing the two:
- Hash mode works out of the box everywhere but can interfere with anchor scrolling and may appear less polished in public-facing URLs.
- History mode supports arbitrary path shapes and allows additional state objects to be attached to history entries. It is the preferred choice for modern deployments, provided the hosting environment is configured with a fallback rewrite rule—such as
try_filesin Nginx or a catch-all directive in Apache—that delegates unknown paths to the application entry point.