Flash messages connect a Laravel action to the next screen. Inertia keeps the request flow server-driven, while Vue presents the result as a toast.
The basic pattern is short:
return redirect()
->route('projects.index')
->with('success', 'Project created successfully.');
Laravel flashes the message to the session. Inertia returns the redirect response. Vue reads the message and renders a small notification.
This approach fits the strengths of Laravel as a PHP web framework. Your controller owns the action. Your Vue component owns the interaction. You avoid a second notification API for ordinary web requests.
Session flash data: The server-side contract
Laravel session flash data is designed for short-lived values. A flashed value is available during the current request and the next HTTP request. Laravel removes it after that request completes.
You can flash a value explicitly:
public function store(Request $request)
{
// Store the project...
$request->session()->flash(
'success',
'Project created successfully.'
);
return redirect()->route('projects.index');
}
You can also flash data while building a redirect:
public function store(Request $request)
{
// Store the project...
return redirect()
->route('projects.index')
->with('success', 'Project created successfully.');
}
The two examples use the same session mechanism. Prefer ->with() when the message belongs directly to the redirect. Use session()->flash() when you need to prepare several values before returning.

Keep notification data small. A message, type, and optional identifier are usually enough.
Do not place permanent application state in flash data. Use a database record or regular Inertia prop for data that must survive multiple requests.
Shared props: Expose flash data to Vue
Laravel stores the message in the session. Vue cannot read the session directly. The Inertia middleware must expose selected session values as shared props.
Open app/Http/Middleware/HandleInertiaRequests.php and extend the share() method:
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Request;
use Inertia\Middleware;
class HandleInertiaRequests extends Middleware
{
public function share(Request $request): array
{
return array_merge(parent::share($request), [
'flash' => [
'success' => fn () => $request->session()->get('success'),
'error' => fn () => $request->session()->get('error'),
'info' => fn () => $request->session()->get('info'),
],
]);
}
}
The closures keep the values lazy. The flash namespace also prevents collisions with page-specific props.
Inertia merges shared data with the props returned by your controller. Vue can now access the values through usePage():
<script setup lang="ts">
import { computed } from 'vue'
import { usePage } from '@inertiajs/vue3'
const page = usePage()
const flash = computed(() => page.props.flash)
</script>
<template>
<div v-if="flash.success" role="status">
{{ flash.success }}
</div>
</template>
The official Inertia shared data documentation recommends using shared data sparingly. Flash messages are a good exception because they belong in a global layout and appear across many pages.
Toast component: Keep the UI small
A toast needs three concerns. It displays a message, communicates its status, and lets the user dismiss it.
Create resources/js/Components/Toast.vue:
<script setup lang="ts">
defineProps<{
message: string
type?: 'success' | 'error' | 'info'
}>()
defineEmits<{
close: []
}>()
</script>
<template>
<aside
class="fixed right-6 top-6 z-50 flex max-w-sm items-start gap-4 rounded-lg border bg-white p-4 shadow-lg"
role="status"
aria-live="polite"
>
<div class="flex-1">
<p class="text-sm font-medium">
{{ message }}
</p>
</div>
<button
type="button"
class="text-sm text-gray-500 hover:text-gray-900"
aria-label="Dismiss notification"
@click="$emit('close')"
>
Dismiss
</button>
</aside>
</template>
You can add type-specific classes later. Start with one component. The notification system should not become a second frontend framework.
Render the toast from a persistent layout such as AppLayout.vue:
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { usePage } from '@inertiajs/vue3'
import Toast from '@/Components/Toast.vue'
type FlashMessage = {
message: string
type: 'success' | 'error' | 'info'
id?: string
}
const page = usePage()
const toast = ref<FlashMessage | null>(null)
const flash = computed(() => page.props.flash)
let lastFlashKey: string | null = null
watch(
flash,
(value) => {
if (!value) {
return
}
const message = value.success
? { type: 'success' as const, message: value.success }
: value.error
? { type: 'error' as const, message: value.error }
: value.info
? { type: 'info' as const, message: value.info }
: null
if (!message) {
return
}
const key = `${message.type}:${message.message}`
if (key === lastFlashKey) {
return
}
lastFlashKey = key
toast.value = message
},
{ immediate: true, deep: true }
)
</script>
<template>
<Toast
v-if="toast"
:message="toast.message"
:type="toast.type"
@close="toast = null"
/>
<slot />
</template>
The watcher reacts whenever Inertia updates the page props. The lastFlashKey guard prevents the same message from triggering repeatedly while the layout remains mounted.
For messages that may repeat with identical text, send a unique identifier from the controller:
use Illuminate\Support\Str;
return redirect()
->route('projects.index')
->with('flash', [
'id' => (string) Str::uuid(),
'type' => 'success',
'message' => 'Project created successfully.',
]);
Then share the structured flash value instead:
'flash' => fn () => $request->session()->get('flash'),
This gives the frontend a reliable consumption key.
Inertia 3 flash data: Avoid browser history repeats
Inertia 3.x also provides dedicated flash data. It is intended for one-time values such as toast messages and success alerts.
Unlike regular shared props, Inertia flash data is not persisted in browser history. This makes it a better fit when users navigate backward and forward through an application.
The server can flash a toast with Inertia::flash():
use Inertia\Inertia;
public function store(Request $request)
{
// Store the project...
return Inertia::flash('toast', [
'type' => 'success',
'message' => 'Project created successfully.',
])->back();
}
In Vue, this data is available through page.flash, not page.props.flash:
<script setup lang="ts">
import { usePage } from '@inertiajs/vue3'
const page = usePage()
</script>
<template>
<div v-if="page.flash.toast" role="status">
{{ page.flash.toast.message }}
</div>
</template>
You can also listen for the global flash event. This works well for a central toast host:
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue'
import { router } from '@inertiajs/vue3'
const emit = defineEmits<{
toast: [message: {
type: 'success' | 'error' | 'info'
message: string
}]
}>()
let removeListener: (() => void) | undefined
onMounted(() => {
removeListener = router.on('flash', (event) => {
const toast = event.detail.flash.toast
if (toast) {
emit('toast', toast)
}
})
})
onUnmounted(() => {
removeListener?.()
})
</script>
The Inertia flash data documentation covers the page.flash object, the onFlash callback, and the global event.
Use one strategy per notification path. Do not expose the same session key through page.props.flash and page.flash unless you need both interfaces.
Resetting flashes: Let the session expire
Laravel handles the server-side lifecycle automatically when you use flash() or redirect with().
Do not replace flash data with session()->put() for temporary notices. Regular session values persist until you remove them. That often causes old success messages to appear on later screens.
You can explicitly remove a session value when required:
$request->session()->forget('success');
You can retrieve and remove a value in one operation:
$message = $request->session()->pull('success');
Do not use pull() inside the shared-props middleware if the page still needs that value. The middleware would consume it before Vue receives the response.
On the client, clearing toast.value removes the visible notification. It does not change the server session. Server consumption comes from Laravel’s flash lifecycle. Browser-history protection comes from Inertia 3 flash data or a client-side deduplication key.
Breeze integration: Follow the existing layout
Laravel Breeze applications already follow this architecture. Controllers handle redirects. HandleInertiaRequests shares global props. Vue layouts wrap the page content.
Place the toast host in resources/js/Layouts/AuthenticatedLayout.vue if only authenticated users need notifications:
<script setup lang="ts">
import Toast from '@/Components/Toast.vue'
import { computed, ref, watch } from 'vue'
import { usePage } from '@inertiajs/vue3'
const page = usePage()
const toast = ref<{ type: string; message: string } | null>(null)
const flash = computed(() => page.props.flash)
watch(
flash,
(value) => {
if (value?.success) {
toast.value = {
type: 'success',
message: value.success,
}
}
},
{ immediate: true }
)
</script>
<template>
<Toast
v-if="toast"
:message="toast.message"
:type="toast.type"
@close="toast = null"
/>
<div>
<slot />
</div>
</template>
For public and authenticated pages, place it in the root layout instead. The Laravel starter kit documentation describes the same layout-based customization model for Vue and Inertia applications.
Progressive enhancement: Keep the redirect
Inertia intercepts form submissions when JavaScript is available. Your Laravel controller still processes the request and returns the redirect.
Without JavaScript, the same controller can return a normal browser redirect. A Blade fallback can render the session message:
@if (session('success'))
<div role="status">
{{ session('success') }}
</div>
@endif
This gives you a graceful fallback without creating two separate controller workflows.
The pattern also keeps your web routes distinct from API routes. If you build a REST API with PHP, return JSON from API endpoints rather than relying on session flash data. Your Vue client can then map the JSON response to the same toast component.
Laravel’s elegant request flow is the useful part here. A PHP developer can send one redirect message from a controller. Inertia carries it to Vue. A small component completes the experience.
The best PHP developer tools often remove coordination work rather than adding features. Flash messages are a small example. They keep server actions, browser navigation, and frontend feedback aligned.
If you have a different flash-message pattern in production, we’d love to hear how you handle it.