A Livewire modal that stays open through re-renders
A Blade modal for Livewire 3 and 4: open and close it from the component, keep it open while validation errors show, and reset its form on close. No Alpine needed.
php artisan bladewell:add modal text-input button
Uses Modal, Text input and Button. New to Bladewell? Install it first.
Why modals close on their own
Livewire re-renders a component by comparing the new HTML with the page and patching the difference. A modal whose open state lives only in the browser, as a class or an attribute set by a script, can be patched back to closed. It happens on the very render that should show "The email has already been taken." The visitor sees the modal vanish and never sees the error.
Bladewell's modal is a native <dialog> that keeps its own state through every Livewire 3 and 4 render, checked in a real browser. It needs no Alpine, and nothing from Livewire, so the same tag works on a plain Blade page.
A form in a modal
Any element with data-modal-open opens it, and data-modal-close closes it, no wire:click or server round trip. The Save button sits in the footer, outside the form, so it names the form with form="profile-form".
<div>
<x-widget.button data-modal-open="edit-profile">Edit profile</x-widget.button>
<x-widget.modal id="edit-profile" title="Edit profile">
<form id="profile-form" wire:submit="save" class="grid gap-4">
<x-widget.text-input label="Name" wire:model="name" required />
<x-widget.text-input label="Email" type="email" wire:model="email" required />
</form>
<x-slot:footer>
<x-widget.button variant="neutral" data-modal-close>Cancel</x-widget.button>
<x-widget.button type="submit" form="profile-form">Save</x-widget.button>
</x-slot:footer>
</x-widget.modal>
</div>
The component validates as usual. A failed save re-renders with the errors under their fields and the modal still open. A good one closes it from the server.
namespace App\Livewire;
use Illuminate\Validation\Rule;
use Livewire\Component;
class EditProfile extends Component
{
public string $name = '';
public string $email = '';
public function mount(): void
{
$this->fill(auth()->user()->only('name', 'email'));
}
public function save(): void
{
$validated = $this->validate([
'name' => ['required', 'string', 'max:120'],
'email' => ['required', 'email', Rule::unique('users')->ignore(auth()->id())],
]);
auth()->user()->update($validated);
// Closes it from the server, once the save went through.
$this->dispatch('modal-close', id: 'edit-profile');
}
public function render()
{
return view('livewire.edit-profile');
}
}
Open and close it from PHP
Dispatch modal-open or modal-close with the modal's id, from any action. Use it to open a modal once a record has loaded, or to close one after a save.
// Anywhere in the component: open it, or close it.
$this->dispatch('modal-open', id: 'edit-profile');
$this->dispatch('modal-close', id: 'edit-profile');
Start empty every time
A "New project" modal shouldn't reopen with the last project's name in it. reset-on-close puts its forms back to their starting values when it closes, and empties the wire:model properties too.
<x-widget.modal id="new-project" title="New project" reset-on-close>
<form id="project-form" wire:submit="create" class="grid gap-4">
<x-widget.text-input label="Name" wire:model="projectName" required />
</form>
<x-slot:footer>
<x-widget.button variant="neutral" data-modal-close>Cancel</x-widget.button>
<x-widget.button type="submit" form="project-form">Create</x-widget.button>
</x-slot:footer>
</x-widget.modal>
A modal only your code can close
While something runs that mustn't be interrupted, disable-close removes the X, Esc and the backdrop click. Dispatching modal-close from the component still closes it, because the app asked.
<x-widget.modal id="importing" title="Importing your contacts" disable-close>
<p>This takes a minute. Keep the page open.</p>
</x-widget.modal>
"Are you sure?"
Livewire's wire:confirm works as it always does, and shows the browser's own dialog:
<x-widget.button variant="danger" wire:click="delete" wire:confirm="Delete this project?">Delete</x-widget.button>
For a styled one that matches your modals, call modal.confirm() from a script. It needs no markup, starts on Cancel so Enter can't confirm by accident, and resolves true or false. See the confirm example.
Without Livewire
On a plain Blade page the form posts as normal. Pass :open="$errors->any()" and the modal opens again after a failed submit, with the errors and old input in place.
{{-- Opens again after a failed submit, with the errors in place. --}}
<x-widget.modal id="edit-profile" title="Edit profile" :open="$errors->any()">
<form id="profile-form" method="POST" action="{{ route('profile.update') }}" class="grid gap-4">
@csrf
@method('PATCH')
<x-widget.text-input name="name" label="Name" :value="$user->name" required />
<x-widget.text-input name="email" type="email" label="Email" :value="$user->email" required />
</form>
<x-slot:footer>
<x-widget.button variant="neutral" data-modal-close>Cancel</x-widget.button>
<x-widget.button type="submit" form="profile-form">Save</x-widget.button>
</x-slot:footer>
</x-widget.modal>
Drawers, full-screen dialogs and bottom sheets on phones are the same component. See the modal's page.