Wiremodal

Wiremodal is a modal system for Laravel that works with or without Livewire and with or without Alpine. It ships a small vanilla JS controller, its own CSS themes and zero runtime dependencies, and shares the data-wire-theme attribute with wiretoast and wirecookies.
Installation
composer require edulazaro/wiremodal
php artisan vendor:publish --tag=wiremodal-assets
The service provider registers itself through the package's extra.laravel.providers. There is no config file, because the package ships no config/ directory: everything configurable lives on component props or in CSS variables.
The publish command drops CSS and JS into public/vendor/wiremodal:
<link rel="stylesheet" href="{{ asset('vendor/wiremodal/css/wiremodal.css') }}">
<script src="{{ asset('vendor/wiremodal/js/wiremodal.js') }}" defer></script>
With Vite you can skip publishing and import straight from vendor, so package updates flow through without re-publishing:
/* resources/css/app.css */
@import "../../vendor/edulazaro/wiremodal/resources/css/wiremodal.css";
// resources/js/app.js
import '../../vendor/edulazaro/wiremodal/resources/js/wiremodal.js';
Two other publish tags exist: wiremodal-views publishes the Blade view to resources/views/vendor/wiremodal for customizing the markup, and wiremodal publishes assets and views at once.
Defining a modal
<x-wiremodal name="confirm-delete" title="Delete record?" size="sm">
<x-slot:body>
<p>This action cannot be undone.</p>
</x-slot:body>
<x-slot:footer>
<button type="button" data-wm-dismiss>Cancel</button>
<button type="button" onclick="Wiremodal.close('confirm-delete', 'confirmed')">
Delete
</button>
</x-slot:footer>
</x-wiremodal>
Any element carrying data-wm-dismiss closes the modal it lives inside, the same mechanism the built-in close X and the overlay use, so you never write a cancel handler.
Opening and closing
From a Livewire component, via the registered macros:
$this->openModal('confirm-delete');
$this->closeModal('confirm-delete');
From plain JavaScript or Alpine, via the frozen Wiremodal global:
Wiremodal.open('confirm-delete');
Wiremodal.close('confirm-delete');
Both paths reach the same vanilla controller. The modal does not care how it was opened.
The Promise API
Wiremodal.open() returns a Promise that resolves with whatever value you later pass to Wiremodal.close(name, result):
const result = await Wiremodal.open('confirm-delete');
if (result === 'confirmed') {
await deleteRecord();
}
Cancel, the X, the overlay click and the ESC key all close with no result, so result comes back undefined.
Internally a pendingResolvers map keyed by modal name stashes the Promise's resolve function on open(), and close(name, result) looks it up and calls it.
Blocking a close
wiremodal:beforeclose fires just before a modal closes and is cancelable:
modal.addEventListener('wiremodal:beforeclose', e => {
if (formIsDirty) {
e.preventDefault();
alert('Save or discard your changes first');
}
});
The event detail carries a reason so you can guard selectively:
| Reason | Cause |
|---|---|
dismiss |
The X or the overlay |
escape |
The ESC key |
programmatic |
An explicit close() call |
Theming
Set data-wire-theme on an ancestor, usually <html>, and every wire* component on the page follows.
<html data-wire-theme="claude" data-wire-theme-mode="dark">
There are eleven built-in themes. Dark mode is either data-wire-theme-mode="dark" or a .dark class on an ancestor.
If no preset fits, override CSS variables directly: the shared --wire-* ones for the whole family, or the modal-specific --wm-* ones for finer control. No PHP is involved, on purpose.
built and maintained by Edu Lazaro · MIT license