Modal Component
Overview
Welcome to this advanced modal component engineered for complete flexibility and designed to be used seamlessly from anywhere in your application.
Installation
Use the fluxtor artisan command to install the modal
component easily:
php artisan fluxtor:install modal
then you need to import the ./globals/modals.js
in your app.js like so:
// ... import './globals/modals.js'; // ...
Basic Usage
All you need is to bind a trigger to a modal using a unique id
. That's it.
<x-ui.modal.trigger id="basic-modal"> <x-ui.button> Open </x-ui.button> </x-ui.modal.trigger> <x-ui.modal id="basic-modal" heading="Basic Modal" description="This is a simple modal example" > <p>Modal content goes here...</p> </x-ui.modal>
This component is designed to be controlled globally via events while supporting isolated scoped instances, giving you maximum control and flexibility for any UI scenario.
When using modals inside loops or repeated components, it's critical to generate unique modal ids dynamically. Without unique identifiers, a single modal trigger will open all modals sharing the same id on the page, leading to unpredictable and unwanted behavior.
@foreach ($posts as $post) <x-ui.modal :id="'edit-post-' . $post->slug"> <!-- You may put the trigger here, and bind to it the same id --> </x-ui.modal> @endforeach
Usage with Livewire
Just dispatch events like you would do normally:
use Livewire\Component; class CreatePost extends Component { public function update() { // ... $this->dispatch('open-modal', id: 'confirm-update'); } }
See an example of how to build a confirmation modal below
Usage with Blade
<x-ui.button x-on:click="$modal.open('confirm-update')" > Open confirm update modal </x-ui.button> <x-ui.button x-on:click="$modal.close('confirm-update')" > Close confirm update modal </x-ui.button> <x-ui.button x-on:click="$modal.closeAll()" > Close all modals </x-ui.button>
See a deeper look at modal store below
Positioning
By default, the modal is aligned vertically to the top, but you can also position it at center or bottom.
Center & Bottom Positioning
<div class="flex gap-2 justify-center"> <x-ui.modal.trigger id="center-position" class="my-4"> <x-ui.button> Center Modal Trigger </x-ui.button> </x-ui.modal.trigger> <x-ui.modal.trigger id="bottom-position" class="my-4"> <x-ui.button> Bottom Modal Trigger </x-ui.button> </x-ui.modal.trigger> </div> <!-- CENTER POSITION MODAL --> <x-ui.modal id="center-position" position="center" heading="Center Modal" description="This modal is centered vertically" > <p>Modal content goes here...</p> </x-ui.modal> <!-- BOTTOM POSITION MODAL --> <x-ui.modal id="bottom-position" position="bottom" heading="Bottom Modal" description="This modal appears at the bottom" > <p>Modal content goes here...</p> </x-ui.modal>
Slideover
You can transform the overlay modal into a slideover using the slideover
prop:
<x-ui.modal.trigger id="slideover-demo"> <x-ui.button> Open Slideover </x-ui.button> </x-ui.modal.trigger> <x-ui.modal id="slideover-demo" heading="Slideover Modal" description="This is a slideover example" slideover > <p>Slideover content goes here...</p> </x-ui.modal>
Backdrop Options
By default, the modal uses a small blur effect as backdrop. In addition, you can choose between transparent
and dark
variants:
<div class="flex gap-2 justify-center"> <x-ui.modal.trigger id="transparent-backdrop" class="my-4"> <x-ui.button> Transparent Backdrop </x-ui.button> </x-ui.modal.trigger> <x-ui.modal.trigger id="dark-backdrop" class="my-4"> <x-ui.button> Dark Backdrop </x-ui.button> </x-ui.modal.trigger> </div> <!-- TRANSPARENT BACKDROP MODAL --> <x-ui.modal id="transparent-backdrop" backdrop="transparent" heading="Transparent Backdrop" description="This modal has a transparent backdrop" > <p>You can see the background clearly through the transparent backdrop.</p> </x-ui.modal> <!-- DARK BACKDROP MODAL --> <x-ui.modal id="dark-backdrop" backdrop="dark" heading="Dark Backdrop" description="This modal has a dark backdrop" > <p>The background is darkened for better focus.</p> </x-ui.modal>
Need a custom backdrop effect? You own the code, go tweak it for your needs!
Animation Options
Control how your modal appears and disappears with different animation styles:
<!-- Scale Animation (default) --> <x-ui.modal id="scale-animation" animation="scale" heading="Scale Animation" > <p>Scales from 95% to 100% size</p> </x-ui.modal> <!-- Slide Animation --> <x-ui.modal id="slide-animation" animation="slide" heading="Slide Animation" > <p>Slides in from the right</p> </x-ui.modal> <!-- Fade Animation --> <x-ui.modal id="fade-animation" animation="fade" heading="Fade Animation" > <p>Simple opacity transition</p> </x-ui.modal>
Modal Sizes
Control the width of your modals with predefined size options:
<!-- Available width options --> <x-ui.modal width="xs" /> <!-- max-w-xs --> <x-ui.modal width="sm" /> <!-- max-w-sm (default) --> <x-ui.modal width="md" /> <!-- max-w-md --> <x-ui.modal width="lg" /> <!-- max-w-lg --> <x-ui.modal width="xl" /> <!-- max-w-xl --> <x-ui.modal width="2xl" /> <!-- max-w-2xl --> <x-ui.modal width="3xl" /> <!-- max-w-3xl --> <x-ui.modal width="4xl" /> <!-- max-w-4xl --> <x-ui.modal width="5xl" /> <!-- max-w-5xl --> <x-ui.modal width="6xl" /> <!-- max-w-6xl --> <x-ui.modal width="7xl" /> <!-- max-w-7xl --> <x-ui.modal width="full" /> <!-- max-w-full --> <x-ui.modal width="screen-sm" /> <!-- max-w-screen-sm --> <x-ui.modal width="screen-md" /> <!-- max-w-screen-md --> <x-ui.modal width="screen-lg" /> <!-- max-w-screen-lg --> <x-ui.modal width="screen-xl" /> <!-- max-w-screen-xl --> <x-ui.modal width="screen-2xl" /> <!-- max-w-screen-2xl --> <x-ui.modal width="screen" /> <!-- fixed inset-0 (full screen) -->
Persistent Modals
Create modals that cannot be closed by clicking away or pressing escape:
<x-ui.modal id="persistent-modal" persistent heading="Persistent Modal" description="This modal can only be closed using the close button" > <p>This modal cannot be closed by clicking away or pressing escape.</p> </x-ui.modal>
Custom Headers and Footers
Custom Footer
Add action buttons and custom footer content:
<x-ui.modal id="custom-footer-modal" heading="Modal with Custom Footer" > <p>Modal content goes here...</p> <x-slot name="footer"> <div class="flex justify-end space-x-3"> <x-ui.button x-on:click="$data.close();" variant="outline"> Cancel </x-ui.button> <x-ui.button x-on:click="$data.close();" variant="primary"> Confirm Action </x-ui.button> </div> </x-slot> </x-ui.modal>
Sticky Headers and Footers
For modals with long content, make headers and footers stick to their positions:
<x-ui.modal id="sticky-modal" heading="Sticky Header and Footer" sticky-header sticky-footer > <!-- Long content that scrolls --> <div class="space-y-4"> <!-- Lots of content here --> </div> <x-slot name="footer"> <!-- Footer buttons stay visible --> </x-slot> </x-ui.modal>
Text Alignment
Control the text alignment within your modal:
<!-- Text alignment options --> <x-ui.modal alignment="left" /> <!-- text-left (default) --> <x-ui.modal alignment="center" /> <!-- text-center --> <x-ui.modal alignment="right" /> <!-- text-right --> <x-ui.modal alignment="start" /> <!-- text-left (alias) --> <x-ui.modal alignment="end" /> <!-- text-right (alias) -->
Close Button Control
Customize or hide the close button:
<!-- Hide the close button --> <x-ui.modal close-button="false" heading="No Close Button" > <p>Modal without close button</p> </x-ui.modal> <!-- With custom close action --> <x-ui.modal heading="Custom Close"> <p>Modal content</p> <x-slot name="footer"> <x-ui.button x-on:click="$data.close();"> Custom Close Button </x-ui.button> </x-slot> </x-ui.modal>
Event Handling
Custom Event Names
Customize the event names used to open and close modals:
<x-ui.modal id="custom-events-modal" open-event-name="show-modal" close-event-name="hide-modal" heading="Custom Events" > <p>This modal uses custom event names</p> </x-ui.modal> <!-- Trigger with custom events --> <x-ui.button x-on:click="$dispatch('show-modal', { id: 'custom-events-modal' })"> Open with Custom Event </x-ui.button>
Modal Event Listeners
Listen to modal open/close events:
<div x-data="{}" @modal-opened.window="console.log('Modal opened:', $event.detail.id)" @modal-closed.window="console.log('Modal closed:', $event.detail.id)"> <x-ui.modal id="event-demo" heading="Event Demo"> <p>Check your browser console for events</p> </x-ui.modal> </div>
Accessibility Features
The modal component includes comprehensive accessibility support:
- ARIA Labels: Properly labeled for screen readers
- Focus Management: Automatic focus handling and restoration
- Keyboard Navigation: Full keyboard support including escape key
- Screen Reader Support: Proper modal role and aria attributes
<x-ui.modal id="accessible-modal" aria-labelledby="custom-label" aria-description="Detailed description for screen readers" heading="Accessible Modal" > <p>This modal has enhanced accessibility features</p> </x-ui.modal>
Advanced Configuration
Disable Specific Close Methods
Fine-tune how users can close your modals:
<x-ui.modal id="restricted-close-modal" close-by-clicking-away="false" close-by-escaping="false" heading="Restricted Close" > <p>This modal can only be closed using the close button</p> </x-ui.modal>
Autofocus Control
Control focus behavior when the modal opens:
<x-ui.modal id="no-autofocus-modal" autofocus="false" heading="No Autofocus" > <p>This modal won't automatically focus on the first input</p> <input type="text" placeholder="This won't be focused automatically" /> </x-ui.modal>
Visibility Control
Control initial visibility:
<x-ui.modal id="initially-hidden-modal" visible="false" heading="Initially Hidden" > <p>This modal is initially hidden from the DOM</p> </x-ui.modal>
$modal
Store
Deeper Look at The $modal
magic utility in Alpine.js is a global reactive store that manages modals across your entire app. Think of it as a central controller that knows which modals are open and which are closed — and it makes opening and closing modals smooth and consistent everywhere.
modal = Alpine.reactive({ openModals: new Set(), open(id) { if (this.openModals.has(id)) return; this.openModals.add(id); window.dispatchEvent(new CustomEvent('open-modal', { detail: { id } })); }, close(id) { if(!this.openModals.has(id)) return; this.openModals.delete(id); window.dispatchEvent(new CustomEvent('close-modal', { detail: { id } })); }, closeAll() { this.openModals.forEach(id => { this.close(id); }); }, getOpenedModals(){ return Array.from(Alpine.raw(this.openModals)); }, isOpen(id) { return this.openModals.has(id); } })
Dealing with stacking context
modals had a very z-index, but if you lock the stacking context in a container, and inside that container you put the modal, the modal maybe behind other element like navbars..., that had higher stacking context order
How it Works
-
openModals
This is aSet
holding the IDs of all currently open modals. Using aSet
ensures no duplicate IDs, so a modal can't be opened twice by accident. -
open(id)
Opens a modal with the givenid
.- First, it checks if this modal is already open. If yes, it does nothing (prevents duplicates).
- If not open yet, it adds the
id
toopenModals
. - Then, it dispatches a global browser event
open-modal
with the modalid
in the event's details. This event lets other parts of your app know to show that modal.
-
close(id)
Closes the modal with the specifiedid
.- Checks if the modal is currently open; if not, does nothing.
- Removes the modal
id
from theopenModals
set. - Dispatches a
close-modal
event with the modalid
so your app can respond by hiding the modal.
-
closeAll()
Closes every open modal by iterating overopenModals
and callingclose(id)
on each. -
getOpenedModals()
Returns an array of all currently open modal IDs.
UsesAlpine.raw()
to get the raw underlying data of the reactiveSet
, then converts it to a normal array for easy use. -
isOpen(id)
Returnstrue
if the modal with thisid
is open; otherwisefalse
.
Quick Example Usage
$modal.open('login'); // Opens the login modal $modal.close('login'); // Closes the login modal console.log($modal.isOpen('login')); // false after close $modal.closeAll(); // Close every open modal instantly console.log($modal.getOpenedModals()); // []
Component Props
Prop Name | Type | Default | Description |
---|---|---|---|
id |
string | null |
Unique identifier for the modal (auto-generated if not provided) |
heading |
string | null |
Modal title text |
description |
string | null |
Modal description text |
width |
string | 'sm' |
Modal width: xs , sm , md , lg , xl , 2xl , 3xl , 4xl , 5xl , 6xl , 7xl , full , screen-sm , screen-md , screen-lg , screen-xl , screen-2xl , screen |
position |
string | 'top' |
Modal position: top , center , bottom |
backdrop |
string | 'blur' |
Backdrop style: blur , dark , transparent |
animation |
string | 'scale' |
Animation type: scale , slide , fade |
slideover |
boolean | false |
Transform modal into a slideover panel |
persistent |
boolean | false |
Prevent closing by clicking away or escape key |
alignment |
string | 'start' |
Text alignment: start , center , end , left , right |
close-button |
boolean | true |
Show/hide the close button in header |
close-by-clicking-away |
boolean | true |
Allow closing by clicking backdrop |
close-by-escaping |
boolean | true |
Allow closing with escape key |
autofocus |
boolean | true |
Auto-focus first focusable element |
sticky-header |
boolean | false |
Make header sticky when scrolling |
sticky-footer |
boolean | false |
Make footer sticky when scrolling |
visible |
boolean | true |
Initial visibility state |
display-classes |
string | 'inline-block' |
CSS classes for display behavior |
open-event-name |
string | 'open-modal' |
Custom event name for opening |
close-event-name |
string | 'close-modal' |
Custom event name for closing |
aria-labelledby |
string | null |
ARIA labelledby attribute |
trigger |
slot | null |
Inline trigger element |
header |
slot | null |
Custom header content |
footer |
slot | null |
Custom footer content |
Trigger Component
The <x-ui.modal.trigger>
component provides a convenient way to create modal triggers:
<x-ui.modal.trigger id="my-modal" class="inline-block" > <x-ui.button>Open Modal</x-ui.button> </x-ui.modal.trigger>
Trigger Props
Prop Name | Type | Default | Description |
---|---|---|---|
id |
string | required | ID of the modal to trigger |
class |
string | '' |
Additional CSS classes |
Best Practices
1. Always Use Unique IDs
<!-- Good --> @foreach($items as $item) <x-ui.modal :id="'edit-item-' . $item->id"> <!-- Modal content --> </x-ui.modal> @endforeach <!-- Bad --> @foreach($items as $item) <x-ui.modal id="edit-item"> <!-- This will cause conflicts --> </x-ui.modal> @endforeach
2. Use Appropriate Sizes
<!-- For simple confirmations --> <x-ui.modal width="sm" /> <!-- For forms --> <x-ui.modal width="lg" /> <!-- For complex content --> <x-ui.modal width="2xl" /> <!-- For mobile-first full-screen experience --> <x-ui.modal width="screen" />
3. Handle Long Content
<!-- For scrollable content --> <x-ui.modal width="lg" sticky-header sticky-footer > <!-- Long content here --> </x-ui.modal>
4. Accessibility Considerations
<!-- Always provide meaningful headings --> <x-ui.modal heading="Delete Account" description="This action cannot be undone" > <!-- Content --> </x-ui.modal> <!-- Use persistent for critical actions --> <x-ui.modal persistent> <!-- Important content that requires user action --> </x-ui.modal>
5. Event Handling Patterns
<!-- Livewire pattern --> <x-ui.button wire:click="$dispatch('open-modal', { id: 'confirm-delete' })"> Delete </x-ui.button> <!-- Alpine.js pattern --> <x-ui.button x-on:click="$modal.open('confirm-delete')"> Delete </x-ui.button> <!-- Custom event pattern --> <x-ui.button x-on:click="$dispatch('show-confirmation')"> Delete </x-ui.button>
This modal component provides the perfect balance of simplicity and power, making it easy to create beautiful, accessible modals while giving you complete control over behavior and appearance.