Updated June 2026. Tested on Vue 3 with Vue Router 4 and
<script setup>. Part of the Techalyst Vue 3 series.
A single-page app swaps views without full page reloads, but it still needs URLs, so pages can be bookmarked, shared, and navigated with the back button. Vue Router is the official plugin that maps each URL to a component. This post covers the foundations: linking, rendering, history modes, and named and nested routes.
The two components
Vue Router gives you two building blocks for the template. <RouterLink> renders a navigation link (an <a> under the hood) and takes a required to prop. <RouterView> is the slot where the matched component renders.
<!-- App.vue -->
<template>
<nav>
<RouterLink to="/">Home</RouterLink>
<RouterLink to="/about">About</RouterLink>
</nav>
<RouterView />
</template>
Clicking a <RouterLink> updates the URL and <RouterView> shows the component that the URL matches. No reloads, real URLs.
Setting up the router
In a Vite project the router lives in its own file. You list routes (a path mapped to a component) and create the router.
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import About from '../views/About.vue'
const routes = [
{ path: '/', name: 'home', component: Home },
{ path: '/about', name: 'about', component: About },
]
export const router = createRouter({
history: createWebHistory(),
routes,
})
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import { router } from './router'
createApp(App).use(router).mount('#app')
Once registered, every component can reach the router through the useRouter and useRoute composables, which we will use below.
History modes and the 404 fallback
The history option decides what the URLs look like. There are two choices.
createWebHashHistory() puts a # in the URL (/#/about). Only the part before the hash reaches the server, so it works with zero server config, but it is poor for SEO.
createWebHistory() gives clean URLs (/about). This is what you want for a real site, with one catch: refreshing /about sends a genuine request to the server, which has no file there and returns 404. The fix is a server catch-all that serves index.html for any unmatched path, letting Vue Router take over.
# Nginx
location / {
try_files $uri $uri/ /index.html;
}
Set up that fallback once and clean URLs behave correctly on refresh and direct visits.
Named routes
Linking by raw path string is fragile: change a path and you must hunt down every <RouterLink> that used it. Give each route a name and link by name instead, then the path can change freely.
<template>
<RouterLink :to="{ name: 'about' }">About</RouterLink>
</template>
Note the : before to, you are passing an object now, not a string, so it must be a binding. Named routes also make passing params and queries cleaner, which the dynamic routes post builds on.
Nested routes
When a section of the app has its own sub-views (a settings page with tabs, say), nest routes. Put a <RouterView> inside the parent component, and give the parent route a children array.
const routes = [
{
path: '/settings',
component: Settings,
children: [
{ path: 'profile', name: 'profile', component: Profile },
{ path: 'billing', name: 'billing', component: Billing },
],
},
]
<!-- Settings.vue -->
<template>
<h1>Settings</h1>
<RouterView /> <!-- profile or billing renders here -->
</template>
Child paths are relative by default, so profile resolves to /settings/profile. Start a child path with / only if you want an absolute path that ignores the parent.
Reading the current route in script
Inside <script setup>, useRoute gives you the current route (params, query, path) and useRouter gives you the instance for programmatic navigation.
<script setup>
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
function goHome() {
router.push({ name: 'home' })
}
</script>
A common project convention is to keep page-level components in a views/ folder (one per route) and reusable pieces in components/. It is just a convention, but it makes a growing app much easier to navigate.
Wrapping up
Vue Router turns component swaps into real, shareable URLs. Use <RouterLink> and <RouterView> in the template, pick createWebHistory for clean URLs (and add the server fallback so refreshes do not 404), and link by name so paths stay easy to change. Nest routes with a child <RouterView> when a section has sub-views, and reach the current route in script with useRoute and useRouter.
More in the series: Vue Router dynamic routes and lazy loading and navigation guards. Questions welcome below.
All comments ()
No comments yet
Be the first to leave a comment on this post.