elegantly/livewire-modal
Elegantly/livewire-modal is a simple, elegant modal package for Laravel Livewire. Build reusable modal components, trigger them from Livewire actions, and add dialogs to existing views with minimal setup. Clean Blade integration makes building and customizing dialogs quick and easy.
Install and publish.
composer require elegantly/livewire-modal
php artisan vendor:publish --tag="modal-config"
php artisan vendor:publish --tag="modal-views" # only if you plan to restyle
Mount the host once. Add the package's modal host to your main layout, just before </body> and outside anything that gets swapped by wire:navigate:
{{-- resources/views/layouts/app.blade.php --}}
@livewireScripts
<livewire:modal />
Look here first: the published config (stack/limits/teleport target) and the base modal class the package ships. Everything else — opening, closing, stacking — flows through the Modal facade and the host component. Skim those two files before reading any docs beyond the README; the surface area is small.
First use case: edit-form modal from an index page.
// app/Livewire/Posts/Index.php
use Elegantly\Modal\Modal;
use App\Livewire\Posts\Edit;
public function edit(int $postId): void
{
Modal::open(Edit::class, ['postId' => $postId]);
}
<button wire:click="edit({{ $post->id }})">Edit</button>
// app/Livewire/Posts/Edit.php — a normal Livewire component
class Edit extends Component
{
public Post $post;
public array $form = [];
public function mount(int $postId): void
{
$this->post = Post::findOrFail($postId);
$this->form = $this->post->only(['title', 'body']);
}
public function save(): void
{
$this->validate(['form.title' => 'required', 'form.body' => 'required']);
$this->post->update($this->form);
Modal::close();
$this->dispatch('post-saved')->to(Index::class);
}
public function render() { return view('livewire.posts.edit'); }
}
That's the whole loop: open with a class + params, render a normal Livewire component inside, close when done, notify the parent by event.
Trigger from Blade without a Livewire method. For static pages, bind the open call to a plain wire:click on a tiny action component, or use the package's Blade trigger/@livewire helper if you don't want a full Livewire parent. Keep triggers as Livewire methods when you can — it keeps authorization in one place.
Confirmation dialogs. Make a generic Confirm component that takes title, message, confirmLabel, and an action identifier, then dispatches a named event back to the caller. Centralizing this means "are you sure?" never grows its own bespoke component per feature.
Parent refresh after mutation. Have the modal dispatch a scoped event (->to(Index::class)) rather than a global one, and listen for it in the parent with #[On('post-saved')]. This avoids the classic bug where a modal closes but the table behind it is stale.
Stacked flows. Because the package supports stacks, model multi-step wizards as a sequence of small components: step one calls Modal::open(StepTwo::class, $payload) and Modal::close(). Each step owns its own validation; the stack handles the back-navigation. Prefer separate classes over one giant component with a $step property — state resetting on close becomes free.
Pass data in, not out. Pass scalars/IDs into Modal::open(), not Eloquent models, and re-fetch inside mount(). It keeps the component testable and avoids stale-model surprises if the record changes while the modal is open.
Reusable chrome. Publish the views and point your own components at a shared modal Blade layout (<x-modal-panel>) so header/footer/close-button styling lives in one place. Do this on day one; retrofitting twenty modals later is tedious.
Testing. Test modal components exactly like any Livewire component (Livewire::test(Edit::class, ['postId' => $post->id])). For the trigger side, assert on the facade/event rather than rendering the host, and only mount the host component in the one or two integration tests that verify the open/close wiring end to end.
<livewire:modal /> instances produce duplicated overlays and confusing close behaviour. Put it in the base layout, never in a page view.wire:navigate and the host. If you use Livewire's SPA navigation, ensure the host sits outside the navigated content, otherwise the container gets destroyed mid-transition and modals appear to "stick" open.Modal::close() and redirect() in the same request and expect a clean unmount — the dispatched close can be lost as the page swaps. Either redirect and let the page reload, or close and emit a parent event.reset() and resetValidation(). The safest habit: treat every modal as ephemeral and never rely on state surviving a close.$errors won't render where the form is. Keep validation next to the form.wire:key on repeated components. Any @foreach that renders modal-triggering items needs a stable wire:key, or Livewire will reuse the wrong component and you'll open the previous record.overflow-hidden on <html> in your own JS, they'll fight and the page will stay locked after close. Pick one owner.php artisan config:clear) → the component class path passed to Modal::open() is fully qualified → a listener isn't swallowing the event. Ninety percent of reports are one of those four.How can I help you explore Laravel packages today?