amjadiqbal/laravel-sonner
Laravel Sonner brings elegant, Sonner-inspired toast notifications to Laravel applications. Flash success, error, and info messages from controllers or Blade views and render them with a clean, customizable API, keeping user feedback consistent and simple to set up.
This is the first stable release of the package, and it is effectively a rewrite: it now ships Sonner 2.0.8's stylesheet plus a plain-JavaScript port of the Toaster, so you get the real Sonner behaviour (stacking, swipe-to-dismiss, hover-to-expand, timers) without React.
Requirements: PHP 8.3+, Laravel 12 or 13. Livewire 3/4 is optional but first-class.
composer require amjadiqbal/sonner
php artisan vendor:publish --tag=sonner-config # check the README for the exact tag
</body>:<x-sonner />
php artisan about — the release adds a line there, which is the fastest way to confirm the package booted and which config is in play.First use case — toast on a redirect from a controller:
public function store(StorePostRequest $request)
{
$post = Post::create($request->validated());
toast()->success('Post saved')->description($post->title);
return redirect()->route('posts.index');
}
The toast is queued during the request, flashed to the session, and rendered on the next full page load by <x-sonner />.
One fluent API, two entry points. The toast() helper and the Sonner facade expose the same chainable surface (success/info/warning/error/loading/message/description/duration/persistent/position/action/cancel/closeButton/richColors/invert/dismissible/className/id). Use the helper for brevity in controllers and the facade where you want an injectable, mockable dependency in services.
Set defaults once, override rarely. Every <Toaster /> prop has a default in config/sonner.php, and the <x-sonner /> component accepts all of them. Configure position/theme/richColors globally, then override per-call only where a specific toast needs to stand out:
<x-sonner position="bottom-right" theme="system" :rich-colors="true" />
Livewire: same-response delivery. Calling toast()->success() inside a Livewire action renders in the same response — no extra round trip, no client-side listener to wire up. On a redirect, the toast waits for the next page. The old UsesSonner trait is gone; just call the helper.
Dispatching from Alpine / Livewire JS. Use the sonner:toast browser event rather than reaching into internals:
$dispatch('sonner:toast', { type: 'success', message: 'Copied' })
// or from a Livewire component
$this->dispatch('sonner:toast', type: 'error', message: 'Upload failed');
Browser API for client-driven flows. The JS surface mirrors Sonner (toast(), toast.success(), toast.promise(), toast.dismiss(), toast.getToasts()). toast.promise() is the workhorse for anything async — uploads, imports, wizard steps — and replaces the removed PHP promise():
toast.promise(saveDocument(), {
loading: 'Saving…',
success: 'Saved',
error: 'Could not save',
});
Progress-style updates with a stable id. Give a loading toast an id and re-use it for the terminal state so the message mutates in place instead of stacking:
toast()->loading('Exporting…')->id('export');
// later
toast()->success('Export ready')->id('export')->action('Download', '/exports/1');
Long-lived notifications. Open toasts survive wire:navigate, so a toast raised before a navigation stays visible — useful for background jobs, but keep <x-sonner /> in the layout only. Mounting it inside a partial that gets swapped will double-render the toaster.
Persistent toasts with actions. Combine persistent(), action(), and closeButton() for "undo" or "review" affordances; the timer-based dismissal is what you're opting out of.
Two breaking changes to sweep for. Delete use AmjadIqbal\Sonner\Livewire\UsesSonner; from any Livewire component, and move any PHP promise() calls to toast.promise() in JavaScript.
Toast text is no longer HTML. Messages are set with textContent and the queue is JSON-encoded with <, >, & and quotes escaped. Anything that previously relied on markup in a toast (links, <strong>, line breaks) will now render literally. If you need structure, keep the text plain and use action()/cancel() for interactivity.
CSP needs a real nonce. The inline style and script are stamped with Laravel's CSP nonce. If you emit your own Content-Security-Policy header outside Laravel's nonce mechanism, or cache pages with a frozen nonce, the toaster's script will be blocked and you'll see nothing at all. First debugging step for "no toasts appear": check the browser console for CSP violations before you touch the config.
Flashed toasts are now consumed on render. Previously a queued toast could survive forever if no page rendered it; now it shows once and leaves the session. The practical consequence: a toast queued for a response that doesn't render <x-sonner /> (a JSON endpoint, a file download, a mail-preview route) can surface later on an unrelated page. Be explicit about where toasts are queued.
Don't double-fire in Livewire. Because same-response delivery is automatic, calling both toast()->success() and a manual $this->dispatch('sonner:toast', …) for the same action gives you two toasts.
Timers pause on hover and in hidden tabs. Short durations are not a reliable cleanup mechanism — a user returning to a backgrounded tab will still see toasts they "should" have missed. Use dismiss() or toast.getToasts() if you need deterministic state.
Escape hatches can trap users. persistent() + dismissible(false) + no closeButton() leaves a toast that only a programmatic dismiss() can remove. Always leave one exit.
The Alt+T hotkey is global. It focuses/expands the toaster region. Check for conflicts with your existing shortcuts, and be aware it is a new keyboard target for accessibility testing.
Dark theme + rich colors changed. Error and warning toasts now use a neutral background instead of Sonner's very dark red and olive. Any custom CSS or visual-regression baselines targeting the old colours need updating — style via className() rather than patching the bundled styles.css, which is Sonner 2.0.8 verbatim and will be replaced on upgrade.
It's a port, not React. The browser API list stops at toast(), success, error, warning, info, loading, promise, dismiss, getToasts. Don't assume React-only affordances like toast.custom with JSX/component content exist — pass strings and use className() for presentation.
Testing. In feature tests, assert on the flashed session payload for PHP-side toasts; for browser behaviour, assert against toast.getToasts() in a Pest browser/Dusk test rather than scraping DOM text, since stacking and animation make the DOM timing-dependent.
How can I help you explore Laravel packages today?