Nested Routes and Layouts
Nest child routes inside a layout with a second router-view, and point old URLs at new ones using redirects and aliases.
{ path: '/settings', component: SettingsLayout, children: [{ path: 'profile', name: 'settings-profile', component: SettingsProfile }] }Screens are usually nested. A settings area has a shared heading and tab bar, and only the panel below changes. Nested routes model that: a record with a children array renders its own component, and that component contains a second <router-view> where the matched child is drawn.
Children and their paths
A child path is relative and gets appended to the parent, so profile under /settings is /settings/profile. A child path that begins with / is treated as absolute and ignores the parent, which is almost never what you meant. Give children the names, not the parent: navigating to a parent with children renders the parent's outlet empty unless a child matches. Add a child with path: '' to decide what /settings itself shows, or redirect it to a real tab.
A layout route
Since Vue Router 4.1 a parent record may omit component and exist only to group children under a prefix and share meta or a beforeEnter guard. Keep the component when there is shared chrome to draw; drop it when there is not, so you avoid a pointless wrapper.
Redirects and aliases
These look similar and are not. redirect sends the browser somewhere else and the URL changes; it takes a path string, a location object, or a function receiving the target route. alias keeps the URL the visitor used and renders the same record, so two addresses are both valid. Redirect when an old URL is retired. Alias when both addresses should keep working.
Guards on a redirecting record do not run; only the target's guards do.
Example
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
</script>
<template>
<section>
<h1>Settings</h1>
<nav>
<router-link :to="{ name: 'settings-profile' }">Profile</router-link>
<router-link :to="{ name: 'settings-billing' }">Billing</router-link>
</nav>
<!-- the matched child route renders here -->
<router-view />
</section>
</template>
<style scoped>
section {
padding: 1rem;
}
nav {
display: flex;
gap: 1rem;
border-bottom: 1px solid currentColor;
padding-bottom: 0.5rem;
}
nav a.router-link-active {
font-weight: 600;
}
</style>When to use it
- Give a settings area one tab bar and swap only the panel below it
- Group every admin screen under one prefix so a single guard covers them all
- Keep an old marketing URL working after the route structure is reorganised
More examples
The records behind that layout
The parent has no name, so nothing can navigate to a URL with an empty panel. The empty-path child is the decision about /settings: here it redirects to the profile tab, and an alternative is to give it a component of its own. redirect: { name: 'settings-profile' } and redirect: '/settings/profile' reach the same place; the named form keeps working if the path is renamed.
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import SettingsLayout from '../views/SettingsLayout.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/settings',
component: SettingsLayout,
children: [
// what /settings itself does
{ path: '', redirect: { name: 'settings-profile' } },
{
path: 'profile',
name: 'settings-profile',
component: () => import('../views/SettingsProfile.vue'),
},
{
path: 'billing',
name: 'settings-billing',
component: () => import('../views/SettingsBilling.vue'),
},
],
},
],
})
export default routerA parent record with no component
Supported since Vue Router 4.1. There is no shared chrome here, so the parent exists only to put both screens under /admin and to attach meta that children inherit through the matched records. Nothing extra is rendered and no second router-view is needed, because the children fill the outlet their nearest rendering ancestor provides.
// src/router/admin.ts
import type { RouteRecordRaw } from 'vue-router'
export const adminRoutes: RouteRecordRaw[] = [
{
path: '/admin',
meta: { requiresAuth: true },
children: [
{
path: '',
name: 'admin-overview',
component: () => import('../views/AdminOverview.vue'),
},
{
path: 'users',
name: 'admin-users',
component: () => import('../views/AdminUsers.vue'),
},
],
},
]Retiring one URL, keeping another
Use the function form of redirect when the new location has to be derived from the old parameters or query; it receives the target route and returns a location. The alias leaves the visitor's URL alone, so an address printed in last year's documentation keeps working without a jump. alias also takes an array when a record needs several addresses. The second record assumes a route named member exists elsewhere in the table.
// src/router/legacy.ts
import type { RouteRecordRaw } from 'vue-router'
export const legacyRoutes: RouteRecordRaw[] = [
// the old address changes to the new one in the address bar
{ path: '/home', redirect: '/' },
// a function redirect can rebuild the target from the old URL
{
path: '/u/:id',
redirect: (to) => ({ name: 'member', params: { id: to.params.id } }),
},
// both /people and /users render the same list, URL untouched
{
path: '/users',
alias: '/people',
name: 'members',
component: () => import('../views/MembersList.vue'),
},
]
Discussion