Everyone loves modals, and with Livewire they're surprisingly easy to implement with the help of Alpine.js. I've been through so many iterations of the perfect modal setup, and *this* is the solution I've arrived at for clean, reusable modals that you can trigger from absolutely anywhere.
This article covers Livewire 2. For the Livewire 3 solution, there's an updated course available here covering everything you need
If you're following along, get yourself a fresh Laravel project set up, install Laravel Breeze for some scaffolding, and install Livewire. This is going to make it really easy to focus on the important bits.
Any modal we create needs the ability to show and hide itself. Rather than duplicate that functionality across multiple Livewire components, start with a base component that you can extend.
Create the component.
php artisan livewire:make Modal
Here's what it needs to look like.
namespace App\Http\Livewire;
use Livewire\Component;
class Modal extends Component
{
public $show = false;
protected $listeners = [
'show' => 'show'
];
public function show()
{
$this->show = true;
}
}
We'll go into more detail on this later, in particular the $listeners
we've added here.
This is the modal that's going to show, so the content is up to you. For this example, let's create a contact us modal.
php artisan livewire:make ContactModal
The code for this is super simple, we just extend the base Modal
we created in the last step.
namespace App\Http\Livewire;
use Livewire\Component;
use App\Http\Livewire\Modal;
class ContactModal extends Modal
{
public function render()
{
return view('livewire.contact-modal');
}
}
Now we actually need to design a modal. I've done the hard work for you here, but you might want to tweak this for your own needs.
First, create a Blade component for the modal so we can easily re-use it.
php artisan make:component Modal
And add the following markup. I'm using Tailwind to style it.
<div class="fixed inset-0 overflow-y-auto px-4 py-6 md:py-24 sm:px-0 z-40">
<div class="fixed inset-0 transform">
<div class="absolute inset-0 bg-gray-500 opacity-75"></div>
</div>
<div class="bg-white rounded-lg overflow-hidden transform sm:w-full sm:mx-auto max-w-lg">
{{ $slot }}
</div>
</div>
The first child div
here is the faded background of the modal (note the opacity-75
class) and the second is the actual content of the modal, added via the default Blade component slot.
Now open up the contact-modal.blade.php
Livewire view and add the following.
<div>
<x-modal>
<div class="p-6">
Contact modal
</div>
</x-modal>
</div>
If you want to a modal to be global to your entire site, you can add it to your app.blade.php
base template. Or, if it belongs in one place only, add it there.
I'm adding it to the bottom of the app.blade.php
file.
{{-- The rest of your app.blade.php file up here --}}
<livewire:scripts />
<livewire:contact-modal />
</body>
</html>
If you refresh now, you should see your modal displayed. Obviously not ideal to have it always display. So, we need to...
Open contact-modal.blade.php
and update it to pass through a Livewire attribute.
<div>
<x-modal wire:model="show"> {{-- <-- Add this --}}
<div class="p-6">
Contact modal
</div>
</x-modal>
</div>
Now update the modal.blade.php
Blade component to take this value and only show the modal if show
is true.
If you're confused at this point, we'll run through this shortly.
<div
x-data="{
show: @entangle($attributes->wire('model')).defer
}"
x-show="show"
x-on:keydown.escape.window="show = false"
class="fixed inset-0 overflow-y-auto px-4 py-6 md:py-24 sm:px-0 z-40"
>
<div x-show="show" class="fixed inset-0 transform" x-on:click="show = false">
<div class="absolute inset-0 bg-gray-500 opacity-75"></div>
</div>
<div x-show="show" class="bg-white rounded-lg overflow-hidden transform sm:w-full sm:mx-auto max-w-lg">
{{ $slot }}
</div>
</div>
Ok, let's break down what's happening.
Modal
component has a show
property (and thus so does our contact modal, because it extends it).wire:model="show"
to tell the Blade component the true/false
value of this.@entangle
. This shares the show
property value from the Livewire component with Alpine.js and keeps it in sync.x-show
conditions to parts of the modal so they're only shown if the show
property is true.x-on:keydown.escape.window
event handler to set show
to false if the escape key is pressed.x-on:click
event handler to set show
to false if the faded background of the modal is clicked (anywhere outside the actual modal content).If you refresh right now, you'll no longer see the modal. Try setting the $show
property on the Modal.php
Livewire component to true
and refreshing again. You should see your modal appear! Don't forget to return it back to false
.
Now, like magic, from anywhere in your app, you can show this modal. Here's an example of a button you could press to trigger the contact modal.
<button x-data="{}" x-on:click="window.livewire.emitTo('contact-modal', 'show')" class="text-indigo-500">
Show contact modal
</button>
This emits a show
event to the contact modal.
Remember the $listeners
from the base Modal
Livewire component? This picks up on the event and sets the $show
property to true. Because we used @entangle
, this keeps in sync with Alpine.js and shows the modal.
You can even trigger the modal directly from another Livewire component.
$this->emitTo('contact-modal', 'show');
The beauty of this solution is that it's quickly reusable. You'll just need to:
Modal
component.Although the solution is simple, if you're new to Livewire or Alpine.js this might be a lot to take in. If you'd like to learn more, we have loads of courses that'll get you up to speed.
Happy modaling!