---
title: "Nested routes and layouts with Vue Router · vuejs.school"
description: "Children routes let one layout wrap many pages. Here is how nested RouterView, relative paths, named views and route.matched fit together."
canonical_url: "https://vuejs.school/community/blog/nested-routes-and-layouts-with-vue-router"
meta:
  "og:description": "Children routes let one layout wrap many pages. Here is how nested RouterView, relative paths, named views and route.matched fit together."
  "og:image": "https://vuejs.school/_og/d/a_~TmVzdGVkIHJvdXRlcyBhbmQgbGF5b3V0cyB3aXRoIFZ1ZSBSb3V0ZXIgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_Nested%2Broutes%2Band%2Blayouts%2Bwith%2BVue%2BRouter%2Cdescription_~Q2hpbGRyZW4gcm91dGVzIGxldCBvbmUgbGF5b3V0IHdyYXAgbWFueSBwYWdlcy4gSGVyZSBpcyBob3cgbmVzdGVkIFJvdXRlclZpZXcsIHJlbGF0aXZlIHBhdGhzLCBuYW1lZCB2aWV3cyBhbmQgcm91dGUubWF0Y2hlZCBmaXQgdG9nZXRoZXIu%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_peach%2Cp_Ii9jb21tdW5pdHkvYmxvZy9uZXN0ZWQtcm91dGVzLWFuZC1sYXlvdXRzLXdpdGgtdnVlLXJvdXRlciI%2Cs_OLzYd3xMvHnRCsSl.png"
  "og:image:alt": "Nested routes and layouts with Vue Router · vuejs.school"
  "og:image:height": "600"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "Nested routes and layouts with Vue Router · vuejs.school"
  "twitter:image": "https://vuejs.school/_og/d/a_~TmVzdGVkIHJvdXRlcyBhbmQgbGF5b3V0cyB3aXRoIFZ1ZSBSb3V0ZXIgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_Nested%2Broutes%2Band%2Blayouts%2Bwith%2BVue%2BRouter%2Cdescription_~Q2hpbGRyZW4gcm91dGVzIGxldCBvbmUgbGF5b3V0IHdyYXAgbWFueSBwYWdlcy4gSGVyZSBpcyBob3cgbmVzdGVkIFJvdXRlclZpZXcsIHJlbGF0aXZlIHBhdGhzLCBuYW1lZCB2aWV3cyBhbmQgcm91dGUubWF0Y2hlZCBmaXQgdG9nZXRoZXIu%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_peach%2Cp_Ii9jb21tdW5pdHkvYmxvZy9uZXN0ZWQtcm91dGVzLWFuZC1sYXlvdXRzLXdpdGgtdnVlLXJvdXRlciI%2Cs_OLzYd3xMvHnRCsSl.png"
  "twitter:image:alt": "Nested routes and layouts with Vue Router · vuejs.school"
---

> Index of every page on this site: [llms.txt](https://vuejs.school/llms.txt)

## Render child routes in the parent's RouterView

A route record (one object in the `routes` array) can carry a `children` array. The parent record matches the start of the URL and renders its component. Inside that component a `<RouterView />` renders whichever child matched the rest.

*src/router/index.ts*

```
import { createRouter, createWebHistory } from 'vue-router'

export const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', name: 'home', component: () => import('@/views/HomeView.vue') },
    {
      path: '/account',
      component: () => import('@/views/account/AccountLayout.vue'),
      children: [
        { path: '', name: 'account', component: () => import('@/views/account/OverviewView.vue') },
        { path: 'profile', name: 'account-profile', component: () => import('@/views/account/ProfileView.vue') },
        { path: 'billing', name: 'account-billing', component: () => import('@/views/account/BillingView.vue') },
      ],
    },
  ],
})
```

The child with `path: ''` is the index child. It renders when the URL is exactly `/account`. Leave it out and `/account` still shows the layout, but with an empty hole where the content should be, because no child matched.

Give the names to the children rather than the parent. Then `{ name: 'account' }` always lands on the overview with the layout around it, and there is no second name that renders the frame on its own.

The layout is an ordinary component with a `<RouterView />` where the page goes:

*src/views/account/AccountLayout.vue*

```
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
</script>

<template>
  <section class="account">
    <h1>Your account</h1>
    <nav aria-label="Account">
      <RouterLink :to="{ name: 'account' }">Overview</RouterLink>
      <RouterLink :to="{ name: 'account-profile' }">Profile</RouterLink>
      <RouterLink :to="{ name: 'account-billing' }">Billing</RouterLink>
    </nav>
    <RouterView />
  </section>
</template>
```

When the user moves from profile to billing, only the inner `RouterView` changes. The layout instance stays mounted, so anything it holds, an open dropdown or a search box, survives the navigation. That is the real win of nesting, beyond typing less markup.

## Relative paths, and the slash that breaks them

Child paths are relative. The router joins the parent's path and the child's path with a slash, so `'billing'` under `/account` becomes `/account/billing`. An easy mistake is to start the child path with a slash:

*src/router/index.ts**Breaks***

```
{
  path: '/account',
  component: AccountLayout,
  children: [
    { path: '/billing', name: 'account-billing', component: BillingView },
  ],
}
```

The [nested routes guide](https://router.vuejs.org/guide/essentials/nested-routes.html) states the rule: nested paths that start with `/` are treated as root paths. So the billing page now lives at `/billing`, and `/account/billing` matches nothing. Open `/account/billing` in development and the console says so: `[Vue Router warn]: No match found for location with path "/account/billing"`.

The page at `/billing` still renders inside `AccountLayout`, because it is still a child record. That is why the rule exists: it lets you nest components without nesting the URL. It is a feature when you choose it and a puzzle when you did not. If you wanted `/account/billing`, drop the slash:

*src/router/index.ts**Works***

```
{
  path: '/account',
  component: AccountLayout,
  children: [
    { path: 'billing', name: 'account-billing', component: BillingView },
  ],
}
```

Params work the same way. A child with `path: 'invoices/:id'` becomes `/account/invoices/:id`, and `route.params.id` is available in the child and in the layout.

## A help panel through named views

Sometimes a page needs to change more than one area of the layout. The billing page wants a help panel beside it; the profile page does not. Named views give a child route more than one component.

*src/router/index.ts*

```
{
  path: 'billing',
  name: 'account-billing',
  components: {
    default: () => import('@/views/account/BillingView.vue'),
    aside: () => import('@/views/account/BillingHelp.vue'),
  },
}
```

The key is `components`, plural. `default` fills the unnamed `<RouterView />`; every other key fills a `<RouterView>` with a matching `name`. So in the layout's template, the single `<RouterView />` becomes two, side by side:

*src/views/account/AccountLayout.vue*

```
<div class="account-grid">
  <RouterView />
  <RouterView name="aside" />
</div>
```

A child that only has `component` leaves the `aside` view empty, which is what the profile page wants. Keep named views to one or two. If a layout needs five of them, the pages differ too much to share a frame, and two layouts will be easier to read.

## Give the public pages a layout route

Nothing about a record says "layout". A layout is a parent record whose component is mostly a `<RouterView />`. That means you can give the public pages their own frame by making `/` the parent:

*src/router/index.ts*

```
routes: [
  {
    path: '/',
    component: () => import('@/layouts/PublicLayout.vue'),
    children: [
      { path: '', name: 'home', component: () => import('@/views/HomeView.vue') },
      { path: 'pricing', name: 'pricing', component: () => import('@/views/PricingView.vue') },
    ],
  },
  {
    path: '/account',
    component: () => import('@/views/account/AccountLayout.vue'),
    children: [/* as above */],
  },
]
```

`PublicLayout.vue` is a header, a `<RouterView />` and a footer. The parent path `/` plus the child path `''` is `/`; plus `'pricing'` is `/pricing`. The user sees no trace of the layout in the address bar, and the two sections of the site have different frames without a single `v-if` in `App.vue`.

## Group routes without a wrapper component

Sometimes you want the grouping without any frame: a shared path prefix or a shared guard, but no extra markup. Since Vue Router 4.1, a parent record may leave out `component` altogether. The router then renders the matched child in the nearest `<RouterView />` above, as if the parent were not there:

*src/router/index.ts*

```
{
  path: '/admin',
  meta: { requiresAuth: true },
  children: [
    { path: '', name: 'admin', component: () => import('@/views/admin/AdminOverview.vue') },
    { path: 'users', name: 'admin-users', component: () => import('@/views/admin/AdminUsers.vue') },
  ],
}
```

Both admin pages render straight into the `<RouterView />` in `App.vue`, and both inherit `requiresAuth` from the parent. The guard below reads it.

## Guard a whole section with route meta

`meta` is a free-form object on a route record for anything the router does not understand itself: whether a page needs a login, a label for the breadcrumb, a page title. Type it once by adding your fields to Vue Router's `RouteMeta` interface with a `declare module` block (a module augmentation), so TypeScript checks every `route.meta` access:

*src/router/index.ts*

```
declare module 'vue-router' {
  interface RouteMeta {
    requiresAuth?: boolean
    breadcrumb?: string
  }
}

router.beforeEach((to) => {
  if (to.meta.requiresAuth && !isSignedIn()) {
    return { name: 'login', query: { redirect: to.fullPath } }
  }
})
```

`isSignedIn()` stands for your own sign-in check, and `login` is a route you add yourself. Returning a location from `beforeEach` redirects there, and the `redirect` query keeps the page the user asked for so the sign-in page can send them back.

Put `meta: { requiresAuth: true }` on the `/account` parent and every child is covered. In Vue Router 4, `to.meta` is a merge of the `meta` of every matched record, parent first and child last, so a child can override a value from its parent and you do not need to loop over the records yourself.

The records behind that merge are in `to.matched`: an array from the outermost record to the innermost, each with its own `path`, `name`, `meta` and `components`. Everything that needs to know "which frame am I in" reads that array.

## Build breadcrumbs from route.matched

Breadcrumbs are the clearest use of `route.matched`. Give the layout record `meta: { breadcrumb: 'Account' }` and the non-index children `meta: { breadcrumb: 'Profile' }` and `meta: { breadcrumb: 'Billing' }`. Skip the index child, because its `path` resolves to the same `/account` as its parent and you would get the label twice. Then one component turns the array into links:

*src/components/AppBreadcrumbs.vue*

```
<script setup lang="ts">
import { computed } from 'vue'
import { RouterLink, useRoute } from 'vue-router'

const route = useRoute()

const crumbs = computed(() =>
  route.matched.flatMap(record =>
    record.meta.breadcrumb ? [{ label: record.meta.breadcrumb, path: record.path }] : [],
  ),
)
</script>

<template>
  <nav aria-label="Breadcrumb">
    <ol>
      <li v-for="(crumb, i) in crumbs" :key="crumb.path">
        <RouterLink v-if="i < crumbs.length - 1" :to="crumb.path">{{ crumb.label }}</RouterLink>
        <span v-else aria-current="page">{{ crumb.label }}</span>
      </li>
    </ol>
  </nav>
</template>
```

`flatMap` does the filtering and the mapping in one pass and keeps the type narrow: with the `RouteMeta` declaration from earlier, TypeScript knows `label` is a `string` because the branch that returns it has already checked it. The last crumb is the current page, so it is text rather than a link, with `aria-current="page"` for screen readers. Render `<AppBreadcrumbs />` in the layouts, above the `<RouterView />`, and it updates on every navigation because `useRoute()` is reactive.

One limit to know: `record.path` for a record with params still contains `:id`. For a breadcrumb that must link to `/account/invoices/42`, give that record a name and build the link with `{ name: record.name, params: route.params }` instead of the raw path.

**Check it stuck**3 questions from the Nested routes and layouts quiz.

[**Practice now**](https://vuejs.school/learn/routing/nested-routes-and-layouts?tab=quiz)

**Was this helpful?**

## **Keep reading**

[Tutorial · 6 min read**Dynamic routes, params and typed routes**](https://vuejs.school/community/blog/dynamic-routes-params-and-typed-routes) [Tutorial · 5 min read**Migrating from unplugin-vue-router to Vue Router 5**](https://vuejs.school/community/blog/migrating-from-unplugin-vue-router-to-vue-router-5)

```json
{"@context":"https://schema.org","@graph":[{"@id":"https://vuejs.school/#website","@type":"WebSite","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school/","publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/nested-routes-and-layouts-with-vue-router#webpage","@type":"WebPage","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-10-02","description":"Children routes let one layout wrap many pages. Here is how nested RouterView, relative paths, named views and route.matched fit together.","name":"Nested routes and layouts with Vue Router","url":"https://vuejs.school/community/blog/nested-routes-and-layouts-with-vue-router","breadcrumb":{"@id":"https://vuejs.school/community/blog/nested-routes-and-layouts-with-vue-router#breadcrumb"},"isPartOf":{"@id":"https://vuejs.school/#website"},"potentialAction":[{"@type":"ReadAction","target":["https://vuejs.school/community/blog/nested-routes-and-layouts-with-vue-router"]}],"primaryImageOfPage":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/#identity","@type":"Organization","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school","logo":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/community/blog/nested-routes-and-layouts-with-vue-router#article","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-10-02","description":"Children routes let one layout wrap many pages. Here is how nested RouterView, relative paths, named views and route.matched fit together.","headline":"Nested routes and layouts with Vue Router","inLanguage":"en","isAccessibleForFree":true,"thumbnailUrl":"https://vuejs.school/_og/d/a_~TmVzdGVkIHJvdXRlcyBhbmQgbGF5b3V0cyB3aXRoIFZ1ZSBSb3V0ZXIgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_Nested%2Broutes%2Band%2Blayouts%2Bwith%2BVue%2BRouter%2Cdescription_~Q2hpbGRyZW4gcm91dGVzIGxldCBvbmUgbGF5b3V0IHdyYXAgbWFueSBwYWdlcy4gSGVyZSBpcyBob3cgbmVzdGVkIFJvdXRlclZpZXcsIHJlbGF0aXZlIHBhdGhzLCBuYW1lZCB2aWV3cyBhbmQgcm91dGUubWF0Y2hlZCBmaXQgdG9nZXRoZXIu%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_peach%2Cp_Ii9jb21tdW5pdHkvYmxvZy9uZXN0ZWQtcm91dGVzLWFuZC1sYXlvdXRzLXdpdGgtdnVlLXJvdXRlciI%2Cs_OLzYd3xMvHnRCsSl.png","@type":["Article","BlogPosting"],"articleSection":["Tutorial"],"author":{"@id":"https://vuejs.school/#identity"},"image":{"@id":"https://vuejs.school/#/schema/image/1"},"isPartOf":{"@id":"https://vuejs.school/community/blog/nested-routes-and-layouts-with-vue-router#webpage"},"mainEntityOfPage":{"@id":"https://vuejs.school/community/blog/nested-routes-and-layouts-with-vue-router#webpage"},"publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/nested-routes-and-layouts-with-vue-router#breadcrumb","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"Blog","item":"https://vuejs.school/community/blog","position":1},{"@type":"ListItem","name":"Nested routes and layouts with Vue Router","position":2}]},{"@id":"https://vuejs.school/#logo","@type":"ImageObject","caption":"vuejs.school","contentUrl":"https://vuejs.school/apple-touch-icon.png","inLanguage":"en","url":"https://vuejs.school/apple-touch-icon.png"},{"@id":"https://vuejs.school/#/schema/image/1","@type":"ImageObject","contentUrl":"https://vuejs.school/_og/d/a_~TmVzdGVkIHJvdXRlcyBhbmQgbGF5b3V0cyB3aXRoIFZ1ZSBSb3V0ZXIgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_Nested%2Broutes%2Band%2Blayouts%2Bwith%2BVue%2BRouter%2Cdescription_~Q2hpbGRyZW4gcm91dGVzIGxldCBvbmUgbGF5b3V0IHdyYXAgbWFueSBwYWdlcy4gSGVyZSBpcyBob3cgbmVzdGVkIFJvdXRlclZpZXcsIHJlbGF0aXZlIHBhdGhzLCBuYW1lZCB2aWV3cyBhbmQgcm91dGUubWF0Y2hlZCBmaXQgdG9nZXRoZXIu%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_peach%2Cp_Ii9jb21tdW5pdHkvYmxvZy9uZXN0ZWQtcm91dGVzLWFuZC1sYXlvdXRzLXdpdGgtdnVlLXJvdXRlciI%2Cs_OLzYd3xMvHnRCsSl.png","inLanguage":"en","url":"https://vuejs.school/_og/d/a_~TmVzdGVkIHJvdXRlcyBhbmQgbGF5b3V0cyB3aXRoIFZ1ZSBSb3V0ZXIgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_Nested%2Broutes%2Band%2Blayouts%2Bwith%2BVue%2BRouter%2Cdescription_~Q2hpbGRyZW4gcm91dGVzIGxldCBvbmUgbGF5b3V0IHdyYXAgbWFueSBwYWdlcy4gSGVyZSBpcyBob3cgbmVzdGVkIFJvdXRlclZpZXcsIHJlbGF0aXZlIHBhdGhzLCBuYW1lZCB2aWV3cyBhbmQgcm91dGUubWF0Y2hlZCBmaXQgdG9nZXRoZXIu%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_peach%2Cp_Ii9jb21tdW5pdHkvYmxvZy9uZXN0ZWQtcm91dGVzLWFuZC1sYXlvdXRzLXdpdGgtdnVlLXJvdXRlciI%2Cs_OLzYd3xMvHnRCsSl.png"}]}
```