Skip to content
Bladewell
Guides

Toast notifications in Laravel and Livewire

Flash messages as toasts: from a controller, a Livewire action, a button or a script, with a loading toast, and read out to screen readers.

php artisan bladewell:add toast button

Uses Toast and Button. New to Bladewell? Install it first.

Once in your layout

Bladewell's toast goes in your layout once. From there it shows Laravel's flash messages on the next page, Livewire's toast events straight away, and anything your own scripts send. Toasts close themselves after three seconds, wait while the pointer is over them or the tab is in the background, and stack in the corner you choose.

resources/views/components/layouts/app.blade.php
{{-- resources/views/components/layouts/app.blade.php --}}
<head>
    {{-- …your head. The CSRF token, for scripts that post (export.js below reads it). --}}
    <meta name="csrf-token" content="{{ csrf_token() }}">
</head>
<body>
    {{ $slot }}

    {{-- Once, just before </body>. top-right by default; also top-left, top-center, center, bottom-left,
         bottom-center or bottom-right. --}}
    <x-widget.toast position="bottom-right" />
</body>

From a controller

Flash success, error, warning or info, the same session flash Laravel already uses, and redirect.

app/Http/Controllers/ProfileController.php
namespace App\Http\Controllers;

use App\Http\Requests\UpdateProfileRequest;
use Illuminate\Http\RedirectResponse;

class ProfileController extends Controller
{
    public function update(UpdateProfileRequest $request): RedirectResponse
    {
        $request->user()->update($request->validated());

        // Flash a message, and it shows as a toast on the page you send them to.
        return back()->with('success', 'Profile updated.'); // or 'error', 'warning', 'info'
    }
}

For a title as well, or another type, flash a toast array:

app/Http/Controllers/OrderController.php
namespace App\Http\Controllers;

use App\Http\Requests\StoreOrderRequest;
use Illuminate\Http\RedirectResponse;

class OrderController extends Controller
{
    public function store(StoreOrderRequest $request): RedirectResponse
    {
        $order = $request->user()->orders()->create($request->validated());

        // With a title, or another type: flash a 'toast' array instead.
        return redirect()->route('orders.show', $order)->with('toast', [
            'type' => 'info',
            'title' => 'Order placed',
            'message' => 'We will email you when it ships.',
        ]);
    }
}

From Livewire

A flash only shows on the next page. In a Livewire action that stays on the page, dispatch a toast event instead: it shows as soon as the action returns. When the action moves to another page, a flash still works, wire:navigate included, and shows once, not again on Back.

app/Livewire/ProjectSettings.php
namespace App\Livewire;

use App\Models\Project;
use Livewire\Component;

class ProjectSettings extends Component
{
    public Project $project;

    public string $name = '';

    public function mount(): void
    {
        $this->authorize('update', $this->project);
        $this->name = $this->project->name;
    }

    public function save(): void
    {
        $this->authorize('update', $this->project);
        $this->project->update($this->validate(['name' => ['required', 'string', 'max:80']]));

        // Shows straight away, with no page load.
        $this->dispatch('toast', type: 'success', message: 'Project saved.');
    }

    public function delete(): void
    {
        $this->authorize('delete', $this->project);
        $this->project->delete();

        // Moving to another page: flash it, and it shows there, with wire:navigate too.
        session()->flash('success', 'Project deleted.');
        $this->redirectRoute('projects.index', navigate: true);
    }

    public function warnAboutStorage(): void
    {
        // Something people must not miss: a title, and it stays until they close it.
        $this->dispatch('toast', type: 'warning', title: 'Almost full', message: 'This project uses 90% of its storage.', autoClose: false);
    }
}

From a button, without a script

For a toast that needs no server, give any element data-toast and data-toast-message. A click shows it.

In any view
{{-- No script of your own: data-toast says the kind, data-toast-message the text. A click shows it. --}}
<x-widget.button data-toast="info" data-toast-message="Link copied.">Copy link</x-widget.button>

{{-- Clears every toast on screen. --}}
<x-widget.button variant="neutral" data-toast="dismiss">Dismiss all</x-widget.button>

From a script, with a loading toast

Scripts call toast.success(), error(), warning() or info(). For something that takes a while, toast.loading() shows a toast that stays, and toast.update() turns it into the result. Here a button with data-export="{{ route('exports.store') }}" starts an export; it's disabled until the export answers, so a second click doesn't start another. It sends the CSRF token from the meta tag in the layout above.

resources/js/export.js
// resources/js/export.js, imported from resources/js/app.js. A loading toast stays until it's updated.
document.addEventListener('click', async (event) => {
    const button = event.target.closest('[data-export]');
    // One export at a time: a second click while it runs does nothing.
    if (!button || button.disabled) {
        return;
    }
    button.disabled = true;
    const id = toast.loading('Preparing your export…');
    try {
        const response = await fetch(button.dataset.export, {
            method: 'POST',
            // From the layout's <meta name="csrf-token">.
            headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content ?? '', Accept: 'application/json' },
        });
        if (!response.ok) {
            throw new Error(response.statusText);
        }
        toast.update(id, 'success', 'Export ready. We have emailed you the link.');
    } catch {
        toast.update(id, 'error', 'The export failed. Try again.');
    } finally {
        button.disabled = false;
    }
});

For screen readers

  • Every toast is read out. Errors and warnings interrupt what's being read; success and info wait their turn.
  • Three seconds is quick to read, especially at a screen reader's pace or with a magnifier. Keep toasts for things people can also see on the page, and give anything they must act on autoClose: false, so it stays until they close it.
  • A failed form still shows its errors under the fields. A toast can say that something went wrong, but not instead of those.