Layouts & Middleware
Nuxt 3 layouts provide a way to wrap pages in shared UI shells -- navigation bars, sidebars, footers -- without repeating markup across every page. Middleware provides programmatic navigation guards that run before a page renders. Together, they form the structural and behavioral backbone of a Nuxt application's routing layer.
The Default Layout
When Nuxt finds a layouts/ directory, it uses the default layout to wrap every page automatically. The layout file must include a <slot /> where page content will be injected.
<!-- layouts/default.vue -->
<script setup lang="ts">
const navItems = [
{ label: 'Home', to: '/' },
{ label: 'Products', to: '/products' },
{ label: 'About', to: '/about' },
]
</script>
<template>
<div class="app-shell">
<header>
<nav>
<NuxtLink v-for="item in navItems" :key="item.to" :to="item.to">
{{ item.label }}
</NuxtLink>
</nav>
</header>
<main>
<slot />
</main>
<footer>
<p>2026 My App</p>
</footer>
</div>
</template>
Every page renders inside <slot /> without any explicit configuration. If no custom layout is specified, default.vue is used.
Custom Layouts
Applications commonly need different layouts for different sections. An admin panel needs a sidebar, an authentication flow needs a minimal centered layout, and marketing pages need a different header.
Create additional layout files in layouts/:
<!-- layouts/admin.vue -->
<script setup lang="ts">
const menuItems = [
{ label: 'Dashboard', to: '/admin' },
{ label: 'Users', to: '/admin/users' },
{ label: 'Settings', to: '/admin/settings' },
]
</script>
<template>
<div class="admin-layout">
<aside class="sidebar">
<NuxtLink v-for="item in menuItems" :key="item.to" :to="item.to">
{{ item.label }}
</NuxtLink>
</aside>
<div class="admin-content">
<slot />
</div>
</div>
</template>
<!-- layouts/auth.vue -->
<template>
<div class="auth-layout">
<div class="auth-card">
<slot />
</div>
</div>
</template>
Assigning Layouts to Pages
Pages opt into a layout using definePageMeta:
<!-- pages/admin/index.vue -->
<script setup lang="ts">
definePageMeta({
layout: 'admin'
})
</script>
<template>
<div>
<h1>Admin Dashboard</h1>
<p>This page uses the admin layout with a sidebar.</p>
</div>
</template>
<!-- pages/login.vue -->
<script setup lang="ts">
definePageMeta({
layout: 'auth'
})
</script>
<template>
<div>
<h1>Sign In</h1>
<form @submit.prevent="handleLogin">
<input type="email" placeholder="Email" />
<input type="password" placeholder="Password" />
<button type="submit">Login</button>
</form>
</div>
</template>
Disabling a Layout
Some pages need no wrapper at all. Set layout: false and the page renders without any layout:
<script setup lang="ts">
definePageMeta({
layout: false
})
</script>
<template>
<div class="fullscreen-presentation">
<h1>Full Screen Content</h1>
</div>
</template>
The NuxtLayout Component
In app.vue, the <NuxtLayout> component is what activates the layout system. It reads the current page's layout metadata and renders the correct layout file.
<!-- app.vue -->
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
Dynamic Layout Switching
Layouts can be changed at runtime using setPageLayout:
<script setup lang="ts">
const useCompactView = ref(false)
function toggleLayout() {
useCompactView.value = !useCompactView.value
setPageLayout(useCompactView.value ? 'compact' : 'default')
}
</script>
<template>
<div>
<button @click="toggleLayout">Toggle Layout</button>
<p>Content here adapts to the active layout.</p>
</div>
</template>
Route Middleware
Middleware functions intercept navigation and can redirect, cancel, or modify it. They are the standard mechanism for route guards in Nuxt.
Named Middleware
Named middleware files live in middleware/ and are referenced by filename:
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
const { loggedIn } = useUserSession()
if (!loggedIn.value) {
return navigateTo('/login')
}
})
<!-- pages/account.vue -->
<script setup lang="ts">
definePageMeta({
middleware: 'auth'
})
</script>
<template>
<div>
<h1>My Account</h1>
</div>
</template>
Global Middleware
Files suffixed with .global run on every route change without explicit registration:
// middleware/01.logging.global.ts
export default defineNuxtRouteMiddleware((to, from) => {
console.log(`Navigating from ${from.path} to ${to.path}`)
})
Global middleware files are sorted alphabetically. Prefix with numbers to control execution order.
Inline Middleware
For one-off guards, define middleware directly inside the page:
<script setup lang="ts">
definePageMeta({
middleware: [
function (to, from) {
const maintenanceMode = useRuntimeConfig().public.maintenanceMode
if (maintenanceMode) {
return navigateTo('/maintenance')
}
}
]
})
</script>
Middleware Execution Order
When multiple middleware apply to a single route, they execute in this order:
- Global middleware (alphabetical by filename)
- Page-defined middleware (in array order)
<script setup lang="ts">
definePageMeta({
middleware: ['auth', 'role-check']
})
</script>
Here, global middleware runs first, then auth, then role-check. If any middleware returns a navigateTo() call, subsequent middleware is skipped.
Auth Middleware Pattern
The most common middleware pattern is authentication and authorization. A robust implementation handles both:
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
const { user } = useAuthState()
if (!user.value) {
return navigateTo({
path: '/login',
query: { redirect: to.fullPath }
})
}
})
// middleware/role.ts
export default defineNuxtRouteMiddleware((to, from) => {
const { user } = useAuthState()
const requiredRole = to.meta.requiredRole as string | undefined
if (requiredRole && user.value?.role !== requiredRole) {
return navigateTo('/unauthorized')
}
})
<!-- pages/admin/users.vue -->
<script setup lang="ts">
definePageMeta({
middleware: ['auth', 'role'],
requiredRole: 'admin'
})
</script>
<template>
<div>
<h1>User Management</h1>
</div>
</template>
The redirect query parameter allows the login page to send users back to their original destination:
<!-- pages/login.vue -->
<script setup lang="ts">
const route = useRoute()
async function handleLogin() {
await performLogin()
const redirect = route.query.redirect as string
navigateTo(redirect || '/')
}
</script>
Navigation Guards in Practice
Middleware can also prevent accidental data loss:
// middleware/unsaved-changes.ts
export default defineNuxtRouteMiddleware((to, from) => {
if (import.meta.server) return
const formStore = useFormStore()
if (formStore.hasUnsavedChanges) {
const confirmed = window.confirm('You have unsaved changes. Leave anyway?')
if (!confirmed) {
return abortNavigation()
}
}
})
abortNavigation() cancels the navigation entirely, keeping the user on the current page. This only works on the client side.
Common Pitfalls
- Forgetting
<slot />in layouts. Without it, page content is never rendered. The layout appears but the page is blank. - Layout name mismatch.
definePageMeta({ layout: 'admin' })expectslayouts/admin.vue. A typo silently falls back to the default layout. - Browser APIs in middleware. Middleware runs on both server and client. Calling
window.confirm()orlocalStoragewithout aimport.meta.clientcheck crashes SSR. - Middleware not returning. If a middleware check fails and you forget to
return navigateTo(...), execution continues to the page. Always return the redirect. - Global middleware performance. Every
.globalmiddleware runs on every navigation. Expensive operations (API calls, heavy computation) in global middleware slow down every page transition. - Circular redirects. If auth middleware redirects to
/loginand/loginalso has middleware that redirects unauthenticated users, you get an infinite loop. Exclude the login page from auth checks.
Key Takeaways
layouts/default.vueautomatically wraps all pages. Custom layouts are assigned withdefinePageMeta({ layout: 'name' }).- Every layout must include
<slot />to render page content. app.vueuses<NuxtLayout>and<NuxtPage />to activate the layout and routing systems.- Middleware comes in three forms: named (reusable files), global (applied everywhere), and inline (defined in the page).
- The auth middleware pattern combines an
authguard for login status with aroleguard for permission checks, usingnavigateTofor redirects andabortNavigationfor cancellation. - Middleware executes globally first (alphabetically), then page-level middleware in array order.