Persistent layouts solve a common problem in Laravel and Vue applications. They keep shared interface elements alive while users move between pages.
Navigation bars, sidebars, audio players, and notification containers can retain their state. Inertia 3.x now makes those layouts easier to configure with layout props.
The simplified feature treats layouts as regular Vue components. Pages can pass static props, derive props from page data, or update them dynamically with setLayoutProps().
This removes several older workarounds. You no longer need an event bus or provide and inject for every small layout change.
Layout props are regular Vue props
An Inertia layout is a normal component. It receives props through the standard Vue mechanism.
<!-- resources/js/Layouts/AppLayout.vue -->
<script setup lang="ts">
withDefaults(defineProps<{
title?: string
showSidebar?: boolean
sidebarCollapsed?: boolean
cartCount?: number
toast?: {
type: 'success' | 'error'
message: string
} | null
}>(), {
title: 'My Application',
showSidebar: true,
sidebarCollapsed: false,
cartCount: 0,
toast: null,
})
</script>
<template>
<div class="min-h-screen">
<header>
<h1>{{ title }}</h1>
<span v-if="cartCount > 0">
Cart ({{ cartCount }})
</span>
</header>
<aside v-if="showSidebar" :class="{ collapsed: sidebarCollapsed }">
<!-- Navigation -->
</aside>
<div v-if="toast" class="toast" :class="toast.type">
{{ toast.message }}
</div>
<main>
<slot />
</main>
</div>
</template>
There is no Inertia-specific hook inside this layout. The layout defines defaults with defineProps() and renders those values like any other component.
This was one of the changes made after the first Inertia 3 beta. The earlier useLayoutProps hook was removed. Layout props now pass directly to the component.
You can read the details in Laravel’s post on what changed since the first Inertia v3 beta, or review the official Inertia layout documentation.

Set static props with a layout tuple
A page can declare a layout as a tuple:
<!-- resources/js/Pages/Dashboard.vue -->
<script setup>
import AppLayout from '@/Layouts/AppLayout.vue'
defineOptions({
layout: [
AppLayout,
{
title: 'Dashboard',
showSidebar: true,
},
],
})
</script>
<template>
<section>
<h2>Recent activity</h2>
</section>
</template>
The first value is the layout component. The second value contains its static props.
These props are set when the layout is defined. They do not change between visits unless dynamic props override them.
Use static props for stable page configuration. A dashboard may always show its sidebar. A settings page may always use a wider content area.
For a layout with a single static value, the tuple remains useful:
<script setup>
import AppLayout from '@/Layouts/AppLayout.vue'
defineOptions({
layout: [AppLayout, { title: 'Settings' }],
})
</script>
Static props sit between layout defaults and dynamic props in the merge order:
- Dynamic props from
setLayoutProps() - Static props from the layout definition
- Defaults declared by the layout component
This gives each page a clear place to define its layout behavior.
Derive props from page data with callbacks
Page data often comes from Laravel. A project page may need to display the project name in the persistent header.
Use a layout callback when the layout props depend on the current page’s props:
<!-- resources/js/Pages/Projects/Show.vue -->
<script setup>
import AppLayout from '@/Layouts/AppLayout.vue'
defineOptions({
layout: (pageProps) => [
AppLayout,
{
title: pageProps.project.name,
showSidebar: true,
},
],
})
</script>
<template>
<article>
<h2>{{ project.name }}</h2>
</article>
</template>
The callback receives the page props. In this example, project.name comes from the Laravel response.
The controller can provide that data normally:
<?php
namespace App\Http\Controllers;
use App\Models\Project;
use Inertia\Inertia;
use Inertia\Response;
class ProjectController
{
public function show(Project $project): Response
{
return Inertia::render('Projects/Show', [
'project' => [
'id' => $project->id,
'name' => $project->name,
'status' => $project->status,
],
]);
}
}
If your application configures a default layout, the callback can return only the props:
<script setup>
defineOptions({
layout: (pageProps) => ({
title: `Project: ${pageProps.project.name}`,
showSidebar: true,
}),
})
</script>
Inertia applies those values to the default layout automatically.
Configure a default layout once
Most authenticated pages use the same application shell. Configure that layout in createInertiaApp() instead of repeating it on every page.
// resources/js/app.js
import { createInertiaApp } from '@inertiajs/vue3'
import AppLayout from '@/Layouts/AppLayout.vue'
createInertiaApp({
resolve: async (name) => {
const pages = import.meta.glob('./Pages/**/*.vue', {
eager: true,
})
return pages[`./Pages/${name}.vue`]
},
layout: () => AppLayout,
setup({ el, App, props, plugin }) {
// Application setup
},
})
The default layout applies to pages that do not declare another layout. A page-level layout always takes precedence.
You can also return no layout for public pages:
createInertiaApp({
resolve: async (name) => {
const pages = import.meta.glob('./Pages/**/*.vue', {
eager: true,
})
return pages[`./Pages/${name}.vue`]
},
layout: (name) => {
if (name.startsWith('Public/')) {
return null
}
return AppLayout
},
})
This keeps the public site separate from the authenticated application shell.
Update layout state with setLayoutProps()
Static props are useful for page configuration. Dynamic props are better when layout state changes during runtime.
<script setup>
import { setLayoutProps } from '@inertiajs/vue3'
setLayoutProps({
title: 'Dashboard',
showSidebar: false,
})
</script>
<template>
<section>
<h2>Dashboard content</h2>
</section>
</template>
Dynamic props target the default layout when no name is provided.
They also have the highest merge priority. A call to setLayoutProps() overrides both tuple props and component defaults.
Inertia resets dynamic layout props when navigating to a new page, unless the visit uses preserveState. Each page can therefore establish the layout state it needs without leaving stale values behind.

Build a route-aware sidebar
A sidebar may be expanded on most screens but collapsed for a dense reporting interface. That decision can come from the current route or Laravel’s server state.
<script setup lang="ts">
import { setLayoutProps, usePage } from '@inertiajs/vue3'
const page = usePage<{
navigation: {
sidebarCollapsed: boolean
}
}>()
setLayoutProps({
showSidebar: true,
})
setLayoutProps('sidebar', {
collapsed:
page.props.navigation.sidebarCollapsed ||
page.url.startsWith('/reports'),
})
</script>
<template>
<section>
<h2>Reports</h2>
</section>
</template>
The named call applies only when the application uses a named sidebar layout. The first argument is optional. Without it, setLayoutProps() targets the default layout.
For server-driven state, Laravel can share the navigation preference:
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Request;
use Inertia\Middleware;
class HandleInertiaRequests extends Middleware
{
public function share(Request $request): array
{
return [
...parent::share($request),
'navigation' => [
'sidebarCollapsed' => fn () =>
(bool) $request->user()?->sidebar_collapsed,
],
];
}
}
Use shared props for data that genuinely belongs to every page. Use layout props for how the shared interface should render.
Target a named layout
Named layouts are useful when a page has multiple persistent shells.
<script setup>
import AppLayout from '@/Layouts/AppLayout.vue'
import SidebarLayout from '@/Layouts/SidebarLayout.vue'
defineOptions({
layout: {
app: [AppLayout, { title: 'Workspace' }],
sidebar: [SidebarLayout, { collapsed: false }],
},
})
</script>
<template>
<h2>Workspace</h2>
</template>
A page can update only the sidebar:
import { setLayoutProps } from '@inertiajs/vue3'
setLayoutProps('sidebar', {
collapsed: true,
})
In Inertia 3.x, this replaces the older setLayoutPropsFor() pattern. The layout name now appears as the optional first argument to setLayoutProps().
Named layouts also make ownership clearer. The application shell owns global navigation. The sidebar layout owns sidebar presentation. A page does not need to know how either component stores its internal state.

Display toasts in a persistent layout
A persistent layout is a natural home for notifications. The page can translate Laravel flash data into a layout prop.
First, share flash data from the Laravel middleware:
public function share(Request $request): array
{
return [
...parent::share($request),
'flash' => [
'success' => fn () => $request->session()->get('success'),
'error' => fn () => $request->session()->get('error'),
],
];
}
Then update the layout from the page:
<script setup lang="ts">
import { onUnmounted, watch } from 'vue'
import { setLayoutProps, usePage } from '@inertiajs/vue3'
const page = usePage<{
flash: {
success?: string
error?: string
}
}>()
const stop = watch(
() => [page.props.flash?.success, page.props.flash?.error],
([success, error]) => {
const message = success ?? error
setLayoutProps({
toast: message
? {
type: success ? 'success' : 'error',
message,
}
: null,
})
},
{ immediate: true },
)
onUnmounted(stop)
</script>
<template>
<section>
<h2>Orders</h2>
</section>
</template>
The layout remains mounted while the page changes. The toast container does not need to be recreated inside every page.
For typed projects, Inertia 3.x supports global layout prop types through InertiaConfig. The TypeScript documentation covers layoutProps and namedLayoutProps.
Keep cart badges in the layout
The same pattern works for cart counts. Laravel can share the current count:
'cart' => fn () => [
'count' => $request->user()
? app(\App\Services\Cart::class)->countFor($request->user())
: 0,
],
A page can mirror that shared value into the layout:
<script setup lang="ts">
import { watch } from 'vue'
import { setLayoutProps, usePage } from '@inertiajs/vue3'
const page = usePage<{
cart: {
count: number
}
}>()
watch(
() => page.props.cart.count,
(count) => setLayoutProps({ cartCount: count }),
{ immediate: true },
)
</script>
After adding an item, reload only the shared cart prop:
import { router } from '@inertiajs/vue3'
router.reload({
only: ['cart'],
})
The layout receives the new badge value without a custom event bus. The page does not need to reach into the layout or expose an injected callback.
Choose the right data boundary
Older Inertia applications often pushed layout data through several layers. Others placed every value in shared props, even when it only controlled one page’s layout.
Layout props give those concerns a direct path.
Use Laravel page props for page content. Use shared data for values needed across the application, such as the authenticated user or cart count. Use layout props for headers, sidebars, toasts, and other persistent shell configuration.
This separation keeps both the PHP backend and Vue components easier to reason about. It also avoids turning provide and inject into a general-purpose state channel.
Inertia 3.x makes persistent layouts feel closer to ordinary Vue composition. As the feature develops, layout props offer a simple foundation for more responsive application shells. Try a tuple for a static header, then add setLayoutProps() where the interface needs to follow route or server state.