router-link and router-view
router-view draws the component that matches the URL, and router-link navigates without a reload while marking itself active.
<router-link :to="{ name: 'about' }">About</router-link> … <router-view /><router-view> is the outlet: the spot in your template where the router renders whichever component matches the current URL. <router-link> is the link: it renders a real <a> element with an href, but intercepts the click and changes the URL through the History API. Nothing reloads, so state elsewhere on the screen survives the navigation.
Two spellings, one component
After app.use(router) both are registered globally. <router-link> and <RouterLink> are the same component, as are <router-view> and <RouterView>; the compiler resolves the kebab-case tag to the PascalCase name. You can also import them from vue-router when you prefer explicit imports.
Shorthand and longhand for the target
to="/about" is a plain string attribute. :to="{ path: '/about' }" is the same destination as a location object, and :to is itself shorthand for v-bind:to. The object form is what you need for a named route with parameters or a query: :to="{ name: 'user', params: { id: '42' }, query: { tab: 'posts' } }". Names survive a path change; hand-written strings do not.
Active classes
While a link's route is part of the current match, the router adds router-link-active to it, and for an exact match it also adds router-link-exact-active. Matching is by route record, not by string prefix, so a parent link stays active on its children but a link to / is not active on /about. Rename the classes per link with active-class and exact-active-class, or for the whole app with linkActiveClass and linkExactActiveClass in createRouter.
Example
<script setup lang="ts">
import { RouterLink } from 'vue-router'
</script>
<template>
<nav>
<!-- shorthand: the destination as a path string -->
<router-link to="/">Home</router-link>
<!-- longhand: the same destination as a location object -->
<router-link :to="{ name: 'reports' }">Reports</router-link>
<!-- the object form is the only one that can carry a query -->
<router-link :to="{ name: 'reports', query: { range: '30d' } }">
Last 30 days
</router-link>
</nav>
</template>
<style scoped>
nav {
display: flex;
gap: 1rem;
}
nav a.router-link-active {
font-weight: 600;
}
nav a.router-link-exact-active {
text-decoration: underline;
}
</style>When to use it
- Build a site navigation bar that highlights the section the reader is in
- Replace anchor tags in a converted static site so navigation stops reloading the page
- Render a sidebar and a main panel from a single URL using two named outlets
More examples
A link that renders its own markup
With the custom prop, router-link renders no element of its own and hands you href, route, navigate, isActive and isExactActive through v-slot. Reach for it when the markup has to be yours: a link inside a list item that needs the state, a tab button, or a wrapper component. You must bind href and the click handler yourself, which is the part people forget. Keep the anchor: dropping it for a div loses keyboard access and middle-click.
<script setup lang="ts">
import { RouterLink } from 'vue-router'
const props = defineProps<{
to: string
label: string
}>()
</script>
<template>
<router-link v-slot="{ href, navigate, isActive }" :to="props.to" custom>
<a
:href="href"
:aria-current="isActive ? 'page' : undefined"
@click="navigate"
>
{{ props.label }}
</a>
</router-link>
</template>
<style scoped>
a[aria-current='page'] {
font-weight: 600;
}
</style>Two outlets for one URL
A template can hold more than one outlet. Give the extra ones a name prop and fill them with the plural components option instead of component; the unnamed outlet is filled by the default key. This is how one URL drives a sidebar and a main panel at once. If a named view has no entry for the current route, that outlet renders nothing rather than erroring.
// src/router/reports.ts
import type { RouteRecordRaw } from 'vue-router'
// ReportsLayout.vue contains:
// <router-view name="filters" />
// <router-view />
export const reportRoutes: RouteRecordRaw[] = [
{
path: '/reports',
component: () => import('../views/ReportsLayout.vue'),
children: [
{
path: '',
name: 'reports',
components: {
default: () => import('../views/ReportsTable.vue'),
filters: () => import('../views/ReportsFilters.vue'),
},
},
],
},
]Renaming the active classes for the whole app
Set these two options once when a design system or a utility-class framework owns the names, instead of repeating active-class on every link. The defaults stay available as a fallback nowhere: once you rename them, router-link-active is no longer added, so any stylesheet still targeting it goes dead.
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import { reportRoutes } from './reports'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
linkActiveClass: 'nav-active',
linkExactActiveClass: 'nav-current',
routes: reportRoutes,
})
export default router
Discussion