bastinald/laravel-livewire-modals
Install and wire it up (5 minutes).
composer require bastinald/laravel-livewire-modals
The package is a thin layer: a single Livewire component (usually livewire:modals) that renders a Bootstrap modal shell, listens for an event, and mounts whatever Livewire component you name into the modal body. So installation is two steps, not one:
<body> (after Bootstrap's JS):{{-- resources/views/layouts/app.blade.php --}}
@livewireScripts
<livewire:modals />
// app/Http/Livewire/Users/Index.php
$this->emit('showModal', 'users.create');
…where users.create resolves to App\Http\Livewire\Users\Create — a perfectly ordinary Livewire component whose Blade view contains just the modal body content (no modal, modal-dialog, or modal-content wrappers; the package owns those).
Where to look first. Open vendor/bastinald/laravel-livewire-modals/src/ before writing any code. There are only a handful of files, and two things matter: the exact event names (show/hide) and the parameter shape the modals component expects. Everything you do day-to-day is built on those two facts, and the README is terse enough that reading the source is faster than guessing. Also check whether the package ships a publishable Blade view — you will almost certainly want to tweak the modal shell (size="lg", centered, scrollable) eventually.
First use case. The canonical one: a "Create User" button on an index page that opens a form modal, submits, closes, and refreshes the table. Build that end-to-end before touching anything else — it exercises every part of the package (event → mount → validate → close → notify parent) and will surface any Bootstrap/JS wiring problems immediately.
Keep modal components small and single-purpose. The modal component is a normal Livewire component, so it has its own mount(), rules(), render(), and lifecycle hooks. Resist the urge to reuse your index component as a modal. The clean split is: parent owns the list and the "open" button; modal owns the form, validation, and persistence; on success the modal emits an event the parent listens for.
class Create extends Component
{
public $name, $email;
public function mount() { $this->reset(['name', 'email']); }
public function save()
{
$this->validate();
User::create(['name' => $this->name, 'email' => $this->email]);
$this->emit('hideModal');
$this->emit('userSaved');
}
}
One modal component, two modes. For create/edit, pass an optional ID and branch in mount():
$this->emit('showModal', ['users.form', $user->id ?? null]);
public function mount($userId = null)
{
$this->user = $userId ? User::findOrFail($userId) : new User;
}
Pass scalar IDs, not Eloquent models — Livewire 2 will serialize public model properties, but you get stale data and bloated payloads, and the authorization check in the modal becomes the only real gate anyway.
Refresh the parent on close. Emit a domain event (userSaved, postDeleted) from the modal and $refresh or re-query in the parent's listener. Don't reach into the parent component directly; the event is the seam, and it keeps the modal reusable from multiple pages.
Confirmation dialogs are a good second use. A tiny modals.confirm component with a message property and an onConfirm event name is worth the 30 lines — it removes every confirm() call from your app and matches the rest of the UI.
Close on validation failure is not automatic. $this->validate() throws a ValidationException which Livewire handles in-place; the modal stays open and errors render. That's the behaviour you want, but only works if your modal Blade includes @error('name') blocks — Bootstrap's is-invalid / invalid-feedback classes are the natural fit.
Trigger from JavaScript when needed. Livewire.emit('showModal', 'users.create') works anywhere in your page JS, which is handy for opening a modal from a data table row action rendered outside Livewire.
Testing. The package adds no test surface of its own. Test the modal component in isolation (Livewire::test(Users\Create::class)->set('name', ...)->call('save')) and assert the parent reacts to the emitted events (->assertEmitted('userSaved')). Skip testing the modal chrome.
Forgetting <livewire:modals /> fails silently. The emit goes nowhere, no error, no modal. This is the single most common "the package is broken" report. If nothing happens, check the layout first, then the browser console for Livewire is not defined (scripts order), then the event name spelling.
Livewire 2 and Bootstrap 4 are the target. The package predates Livewire 3's dispatch() API, so emit() is the dialect throughout. If you're on Livewire 3 you'll be fighting the package; the event bridge still works in compatibility mode but you're better off with a Livewire-3-native modal package. Same story for Bootstrap 4: it needs jQuery and Popper loaded before @livewireScripts; Bootstrap 5 does not, and mixing the two is a classic source of "modal appears but is invisible" (that's the .fade + missing jQuery transition).
Dynamic component state leaks between opens. Livewire caches components by key. If you open the edit modal for user #1, close, then open it for user #2, you can get stale properties. Fix with a wire:key that includes the modal target and params, or a hard $this->reset() in mount() — preferably both.
Nested modals and backdrops. Bootstrap only supports one backdrop cleanly. If you spawn a confirmation modal from inside an edit modal, the second backdrop and z-index will fight the first. Either close the parent first, or set data-backdrop="static" on the inner one and manage the body class (modal-open) yourself. Expect to spend an hour here; budget for it.
Params come from the client. The event payload is browser-controlled. Never let a modal mount() do User::find($id) and assume the current user can edit it — re-authorize inside the modal, every time. This is the package's one real security consideration and it's easy to miss because the trigger looks like server-side code.
Extension points are limited by design. There's no config file to speak of; customization means publishing and editing the modal Blade view, or rolling your own shell that emits the same events. That's fine — the component is ~30 lines — but don't expect a plugin architecture.
Debugging the event bus. Livewire.on('showModal', console.log) in the browser tells you instantly whether the problem is on the emit side (no log → component didn't fire or wasn't hydrated) or the render side (log fires, no modal → modals component missing or view broken). Two minutes saved every time.
Maintenance risk. 68 stars, low activity, no Livewire 3 path. Treat it as a small piece of vendored infrastructure rather than a dependency you'll upgrade forever — and keep your own modal components free of package-specific calls (just events) so swapping it out later is a find-and-replace on two event names rather than a rewrite.
How can I help you explore Laravel packages today?