Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Wiretoast Laravel Package

edulazaro/wiretoast

Wiretoast is a framework-agnostic toast notification system for Laravel, Livewire and Alpine, with pure CSS themes, five semantic types, seven stacked positions, dark mode, progress bars, group mode, and zero JavaScript dependencies.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install. composer require edulazaro/wiretoast — PHP 8.2+, Laravel 11+. Livewire 3 is optional and only required if you want the $this->notify() helper; Alpine and plain JS work without it.

2. Choose one asset path — and commit to it. This is the only decision that's annoying to reverse:

Your project Path Trade-off
Vite (the Laravel 11+ default) @import the CSS in app.css, import the JS in app.js One request, your existing cache-busting, composer update upgrades land automatically
No bundler / quick prototype php artisan vendor:publish --tag=wiretoast-assets + <x-wiretoast :assets="true" /> No build step, but the published copies in public/vendor/wiretoast/ go stale on upgrade

The Vite route is the recommended one and the one that keeps package CSS changes visible in your diffs instead of silently frozen in public/.

3. Render the component exactly once, near the end of your persistent layout, outside any Livewire component:

<x-wiretoast theme="soft" default-position="bottom-right" />

That tag is the entire render surface. There are no partials to include per notification and no Blade work per message.

4. First use case: replace a session()->flash() banner. Open a Livewire component you already have, find a save()-style action, and add one line after the mutation:

public function save()
{
    $this->validate();
    $this->order->save();

    $this->notify('Saved successfully', 'success');
}

Everything else — type, position, timeout, progress bar, group mode — is an option on that same call, not a new concept.

Where to look first when you need more:

  • The live preview demo linked in the README. Picking a theme is a design decision, not a docs decision — flip through the 11 themes and 7 positions visually, then come back for the API.
  • vendor/edulazaro/wiretoast/resources/css/wiretoast.css. That file, not the README, is the source of truth for every --wt-* variable — including the *-tint variants the README only alludes to.
  • The Reference tables. The options object is four keys deep (position, timeout, progress, group); read it once and you're finished.

Style a theme by scoping variables, never by editing the vendored CSS. Override in your own app.css:

[data-wt-theme="brand"] .wt-toast {
    --wt-radius: 8px;
    --wt-bg: #111827;
    --wt-text: #f9fafb;
    --wt-shadow: 0 8px 24px rgba(0, 0, 0, .18);

    --wt-success: #22c55e;
    --wt-error:   #f87171;
}
<x-wiretoast theme="brand" />

Implementation Patterns

One wrapper, three call sites. The API is deliberately identical in PHP, Alpine, and vanilla JS — so you should pick one internal convention and route everything through it. A two-line JS wrapper pays for itself the first time you need global defaults, an environment check, or a swap to a different library:

// resources/js/app.js
import '../../vendor/edulazaro/wiretoast/resources/js/wiretoast.js';

window.toast = (message, type = 'success', options = {}) => {
    window.notify(message, type, { position: 'bottom-right', timeout: 4000, ...options });
};

Mirror it server-side with a trait or base-component method (notifyError($e), notifySaved($model)) so error copy and timeouts live in one place rather than forty call sites.

Pattern: action results. By far the highest-frequency use. Put the toast at the end of the action, after the mutation succeeds — never inside render(), which re-fires on every re-render and floods the viewport.

try {
    $this->order->submit();

    $this->notify(
        ['title' => 'Order submitted', 'message' => "Ref #{$this->order->ref}"],
        'success',
        ['progress' => true, 'timeout' => 5000]
    );
} catch (PaymentException $e) {
    $this->notify('Card declined — try another method', 'error', ['timeout' => 0]);
}

Note timeout => 0 on the failure. A message the user must actually read should not evaporate in five seconds.

Pattern: redirect + toast. notify() is a client-side call, so it dies with the page on a redirect. Flash to the session and dispatch from the destination page:

return redirect()->route('orders.index')
    ->with('toast', ['message' => 'Order created', 'type' => 'success']);
{{-- in the layout, after <x-wiretoast /> --}}
@if (session('toast'))
    <script>window.notify(@js(session('toast.message')), @js(session('toast.type')))</script>
@endif

Pattern: Alpine-only interactions, no server round-trip. Clipboard copies, optimistic UI flips, client-side validation. The Alpine contract is just a browser event, so any element in any Blade view can raise a toast without touching a component class:

<button @click="navigator.clipboard.writeText(url);
               $dispatch('notify', { message: 'Link copied', type: 'neutral' })">
    Copy link
</button>

Pattern: group mode for loops and bulk work. An import that emits one toast per row buries the viewport. Collapse same-type messages into a single toast:

foreach ($rows as $row) {
    $this->import($row);
}

$this->notify("Imported {$count} rows", 'success', ['group' => true]);

Pattern: the toast marks the end of long work. A queue worker can't push to a browser, so the toast becomes a polling concern: poll a status endpoint from Alpine, then dispatch notify on completion. Always pair it with persistent inline state ("Processing…" on the button or row) — the user shouldn't be staring at a blank screen waiting to be told something finished.

Pattern: dynamic theming from your existing switcher. Set it declaratively on the component, or flip it at runtime when you already have a theme toggle:

document.body.dataset.wtTheme = 'neon';

Because it's an attribute on any ancestor, theme selection can live in the same store as your Tailwind dark-mode toggle. No second mechanism, no duplicate state.

Testing: assert the event, not the DOM.

Livewire::test(SaveButton::class)
    ->call('save')
    ->assertDispatched('notify');

For JS-level tests, stub window.notify. The toast markup is vendored presentation code — asserting against it buys you nothing and breaks on every upgrade.

Gotchas and Tips

Double-loading is the number-one self-inflicted bug. Importing the assets via Vite and passing :assets="true" registers the listener twice. You'll see every toast rendered twice, or one that appears and instantly vanishes as the two instances fight. If toasts start duplicating after a refactor, grep your layout for assets= first.

Published assets go stale silently. public/vendor/wiretoast/ does not update on composer update. After upgrading the package you must re-run vendor:publish --tag=wiretoast-assets --force. This is the whole argument for the Vite path.

Build order matters with Vite. Your app.css / app.js import from vendor/, which means npm run build must run after composer install on a fresh checkout or CI runner. If your deploy pipeline builds assets in a separate stage without vendor/, the import fails with a "cannot resolve" error that looks like a config problem but is really a missing dependency tree.

Keep the relative path in CSS; reserve the alias for JS. Vite's @wiretoast alias is convenient for import statements, but CSS @import resolution is fussier about aliases across plugins. @import '../../vendor/edulazaro/wiretoast/resources/css/wiretoast.css'; in app.css is boring and always works — watch the relative depth from resources/css/ (two levels up).

The Livewire default type is success; the JS default is info. Same notify() name, different implicit type. If a message looks unexpectedly green from PHP and blue from Alpine, this is why — pass the type explicitly whenever the semantics matter.

The third argument is overloaded. notify('x', 'success', true) is legacy group mode; notify('x', 'success', ['group' => true]) is the options form. Both work, but mixing styles in one codebase makes true look like a typo. Standardize on the options object and treat the boolean as a migration leftover.

progress needs a timeout to count down. With timeout => 0 (persist) there's no duration for the bar to animate against. Progress bars and persistent toasts are mutually exclusive — pick one per message.

Per-toast position overrides default-position, and center behaves differently from the six edge positions: it doesn't stack, it sits. Don't design a UI around "three toasts centered" — use it for single blocking notices only.

Place <x-wiretoast /> in the persistent layout, not the page view. With wire:navigate, body content is swapped but the layout survives. A container living inside a page view gets torn down mid-navigation and any in-flight toast disappears — or worse, you end up with two containers after a soft navigation.

Watch for a window.notify collision. It's an extremely generic global name, and any other toast/notification script or legacy inline helper will clobber it. If window.notify is not a function, check load order and whether something else defined it first.

Dark mode has three triggers, and Tailwind's .dark class is one of them. Automatic via prefers-color-scheme, forced via <x-wiretoast mode="dark" />, or inherited from a .dark ancestor / [data-wt-theme-mode="dark"]. If your app already toggles a .dark class on <html>, wiretoast follows for free — don't add a second toggle. Just verify the class lands on an ancestor of the toast container.

Setting theme on the component can shadow runtime switching. body[data-wt-theme] is an ancestor attribute, but a theme="flat" prop writes the attribute closer to the toasts. If you plan to switch themes at runtime, either omit the prop entirely or make sure your switcher writes to the same element the component uses.

The Livewire helper isn't magic — confirm it's actually registered. If $this->notify() is undefined in a component, check the vendor source for the trait/concern it ships and use it, or fall back to $this->dispatch('notify', message: '...', type: 'success'), which needs nothing beyond Livewire 3.

Debugging checklist, in order: (1) Is the container actually in the rendered HTML? View source, search for wt-toast. (2) Did the script load — is window.notify a function in the console? (3) Fire window.notify('test', 'success') manually. If that works, the problem is your dispatch path (event name, Livewire helper, page redirect) rather than the package. (4) If it fires but is invisible, inspect the element — a z-index or overflow: hidden ancestor on your own layout is the usual culprit.

Custom themes need the attribute scoping to match. A :root { --wt-* } override is global and will bleed into any other wiretoast instance in the app. Scope under [data-wt-theme="brand"] .wt-toast and pass theme="brand", so the override only exists where you asked for it.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings