Layouts and page meta
Share a frame between pages with layouts, choose one per page with definePageMeta() or per section with a route rule, pass it props, and change it at runtime with setPageLayout().
What you'll learn
When you finish, you can do these.- 1Build a default layout that frames every page
- 2Choose a page's layout and props with definePageMeta()
- 3Give a whole section one layout with a route rule
- 4Change the layout on a click with setPageLayout()
The Corner Shop, the small online shop this track builds, has pages that need different frames. A frame is everything around a page's own content: the shop pages share a header with links and a footer, every admin page needs a sidebar, and the login page needs no frame at all. Copy the frame into each page, and adding one menu link means editing twenty files.
Nuxt keeps each frame in one file, a layout, and each page, or each group of URLs, says which layout it wants. You need one idea from the lesson File-based routing: every file in app/pages/ becomes a page at its own URL, and <NuxtPage /> in app/app.vue, the root component, shows the page that matches the URL. The Code tab holds the shop's files to copy into a Nuxt project, among them two layouts, a home page and a dashboard. By the end, the dashboard picks its own layout and hides its sidebar with a click.
A frame around every page
A layout is a component in the app/layouts/ folder, and the first one to write is app/layouts/default.vue, the frame for every page that asks for no other. The shop's default layout draws a header with two links and a footer:
<template>
<div class="site">
<header>
<NuxtLink to="/">Home</NuxtLink>
<NuxtLink to="/dashboard">Dashboard</NuxtLink>
</header>
<slot />
<footer>Corner Shop</footer>
</div>
</template>
The page renders at <slot />, between the header and the footer: whatever a parent writes between a component's opening and closing tags lands at its <slot />.
Here the parent is app/app.vue. It wraps <NuxtPage />, the page that matches the URL, in <NuxtLayout>, which draws the chosen layout around it:
<template>
<div>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</div>
</template>
At /, the home page does not ask for a layout, so the default one frames it: its Welcome heading appears between the header links and the footer.
Like a page, a layout needs one root element that wraps its whole template, here <div class="site">, and that root cannot be the <slot /> itself.
Pick a layout for a page or a whole section
A second layout gives the admin pages their own frame, and each page or group of URLs picks one. app/layouts/panel.vue draws a sidebar of links and a heading around the page, and it is named panel, after its file:
<script setup lang="ts">
const { title = 'Admin', sidebar = true } = defineProps<{ title?: string, sidebar?: boolean }>()
</script>
<template>
<div class="panel">
<aside v-if="sidebar">
<NuxtLink to="/dashboard">Dashboard</NuxtLink>
<NuxtLink to="/admin/orders">Orders</NuxtLink>
</aside>
<main>
<h1>{{ title }}</h1>
<slot />
</main>
</div>
</template>
It takes two props, values the page passes in. title fills the heading and is Admin by default. sidebar is true by default, and the sidebar shows while it is true.
A page asks for a layout with definePageMeta(). It looks like a function call, but Nuxt lifts it out of the page when it builds the app, so the router knows the layout before any page code runs. The dashboard asks for panel with both props:
<script setup lang="ts">
definePageMeta({
layout: { name: 'panel', props: { title: 'Dashboard', sidebar: true } },
})
</script>
At /dashboard, the panel shows its sidebar and the heading Dashboard above the page's line, Orders today: 12.
A page that passes no props can write the short form, layout: 'panel'. And layout: false turns layouts off, for a login screen that fills the window.
For a whole section of the site, set the layout in one place. A route rule is a setting in nuxt.config.ts for every URL that matches a pattern, and since Nuxt 4.3 its appLayout option gives those pages a layout. '/admin/**' matches every URL under /admin:
export default defineNuxtConfig({
routeRules: {
'/admin/**': { appLayout: 'panel' },
},
})
An orders page added at /admin/orders gets the panel with no definePageMeta at all, headed Admin, the layout's default title.
When several settings apply, Nuxt takes the first it finds: a name prop on <NuxtLayout> in app.vue, then the page's definePageMeta, then the appLayout route rule, then default. So one admin page can still opt out with its own definePageMeta.
Change the layout while the page is open
definePageMeta is settled when the app is built, so it cannot follow a click. To change the layout because of something the visitor does, call setPageLayout(), with props if the layout takes them. On the dashboard, a Hide the sidebar button calls focusMode():
function focusMode() {
setPageLayout('panel', { title: 'Dashboard', sidebar: false })
}
Click it, and the sidebar disappears without a reload, while the panel and its Dashboard heading stay.
To know which layout is showing, useLayout() (Nuxt 4.5) returns its name, or false when the page turned layouts off. app/app.vue prints it in a debug line under the frame:
<script setup lang="ts">
const layout = useLayout()
</script>
<template>
<div>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
<p class="debug">Layout: {{ layout || 'none' }}</p>
</div>
</template>
The debug line reads Layout: panel on the dashboard, and Layout: none on a page with layouts off: layout is false there, so || 'none' takes over. Inside a layout, useLayout() always returns that layout's own name, so call it in app.vue or a page.
What goes wrong
Two mistakes are common: a layout with no single root, and page meta expected to follow a click.
A layout without a single root element warns on every page that uses it. Here the default layout has lost its <div class="site">:
<template>
<header>
<NuxtLink to="/">Home</NuxtLink>
</header>
<slot />
<footer>Corner Shop</footer>
</template>
The pages still render, and the dev console warns with NUXT_E4002 that the default layout "does not have a single root node and will cause errors when navigating between routes". The warning means it: once a layout transition, an animation from one layout to the next, is turned on, moving between pages with different layouts fails. Wrap everything in one element:
<template>
<div class="site">
<header>
<NuxtLink to="/">Home</NuxtLink>
</header>
<slot />
<footer>Corner Shop</footer>
</div>
</template>
With one root, the warning is gone.
The second mistake treats definePageMeta like the rest of the page's code. Here the dashboard keeps sidebar in a ref, a box that holds a value and tells Vue when it changes, and passes its value to the layout:
<script setup lang="ts">
const sidebar = ref(true)
definePageMeta({
layout: { name: 'panel', props: { title: 'Dashboard', sidebar: sidebar.value } },
})
function focusMode() {
sidebar.value = false
}
</script>
Clicking Hide the sidebar runs focusMode(), which sets the ref to false, and the sidebar stays. Nothing warns.
Nuxt lifts definePageMeta out of the page when it builds the app and copies the sidebar declaration out with it, so the meta reads its own copy of the ref, once. The page's ref is a different one, and the layout never sees it.
Keep a plain sidebar: true in the meta, as the Code tab's dashboard does, and make the click call setPageLayout():
function focusMode() {
setPageLayout('panel', { title: 'Dashboard', sidebar: false })
}
The click now reaches the layout, and the sidebar disappears.
What to remember
app.vuewraps<NuxtPage />in<NuxtLayout>. A layout inapp/layouts/renders the page in its<slot />, anddefault.vueapplies unless something else is chosen.- A layout needs one root element, and that root cannot be the
<slot />. definePageMetapicks a page's layout and passes it props;layout: falseturns layouts off.- An
appLayoutroute rule gives a whole section a layout. Nuxt takes the<NuxtLayout>name first, thendefinePageMeta, then the route rule, thendefault. definePageMetais lifted out of the page at build time, so it cannot follow refs. Change the layout while the page is open withsetPageLayout().
In your project
Do these in a real project. Each one says how to confirm it worked.- Create
app/pages/admin/orders.vuewith a single<section>and nodefinePageMeta. Open/admin/orders: the panel frame appears with the title Admin, and the debug line fromapp.vuesaysLayout: panel. Then adddefinePageMeta({ layout: 'default' })to the page: the header and footer replace the panel, because page meta comes before the route rule. - Create
app/pages/login.vuewithdefinePageMeta({ layout: false })and a single<main>. Open/login: no header, no footer, and the debug line fromapp.vuesaysLayout: none. - On
/dashboard, click Hide the sidebar: the sidebar disappears without a reload. Then changesidebar: truetofalsein the page'sdefinePageMetaand reload: the dashboard opens without the sidebar.