schaefersoft/laravel-headless-ui
An accessible modal dialog built on the native <dialog> element. Includes focus trapping, scroll locking, backdrop click, and Escape to close.

Blade components: x-hui::dialog, x-hui::dialog.background, x-hui::dialog.overlay, x-hui::dialog.panel, x-hui::dialog.title, x-hui::dialog.description
Use data-hui-dialog-trigger on any button to open a dialog by its id. Multiple triggers can reference the same dialog.
<button data-hui-dialog-trigger="confirm-dialog">Delete item</button>
<x-hui::dialog id="confirm-dialog">
<x-hui::dialog.background/>
<x-hui::dialog.panel>
<x-hui::dialog.title>Delete item?</x-hui::dialog.title>
<x-hui::dialog.description>This action cannot be undone.</x-hui::dialog.description>
<button data-hui-dialog-close>Cancel</button>
<button data-hui-dialog-close>Confirm</button>
</x-hui::dialog.panel>
</x-hui::dialog>
import { openDialog, closeDialog } from '../../vendor/schaefersoft/laravel-headless-ui/dist/js/hui.js'
openDialog('confirm-dialog')
closeDialog('confirm-dialog')
The dialog can be closed in several ways:
data-hui-dialog-close element inside the dialogEscapecloseDialog('id') from JavaScriptDisable Escape and/or backdrop click to force the user to interact with the dialog content.
<x-hui::dialog id="terms-dialog" :close-on-escape="false" :close-on-backdrop-click="false">
<x-hui::dialog.background class="bg-black/50"/>
<x-hui::dialog.panel>
<x-hui::dialog.title>Accept terms</x-hui::dialog.title>
<x-hui::dialog.description>You must accept to continue.</x-hui::dialog.description>
<button data-hui-dialog-close>I accept</button>
</x-hui::dialog.panel>
</x-hui::dialog>
Lock body scroll while the dialog is open. When multiple scroll-locked dialogs are nested, scroll is restored only after the last one closes.
<x-hui::dialog id="my-dialog" :scroll-lock="true">
...
</x-hui::dialog>
<x-hui::dialog id="welcome-dialog" open>
<x-hui::dialog.panel>
<x-hui::dialog.title>Welcome!</x-hui::dialog.title>
<button data-hui-dialog-close>Got it</button>
</x-hui::dialog.panel>
</x-hui::dialog>
The dialog emits custom events on the <dialog> element:
| Event | When |
|---|---|
hui:dialog:open |
Dialog has opened |
hui:dialog:close |
Dialog has closed |
document.getElementById('confirm-dialog')
.addEventListener('hui:dialog:close', () => {
console.log('Dialog was closed')
})
Add enter/leave transition classes using data attributes. The dialog will animate in on open and wait for the leave transition to finish before closing.
| Attribute | Description |
|---|---|
data-hui-dialog-enter |
Classes applied during the entire enter phase. |
data-hui-dialog-enter-from |
Starting state (applied on first frame). |
data-hui-dialog-enter-to |
Ending state (swapped in on the next frame). |
data-hui-dialog-leave |
Classes applied during the entire leave phase. |
data-hui-dialog-leave-from |
Starting state of the leave transition. |
data-hui-dialog-leave-to |
Ending state (swapped in on the next frame). |
<x-hui::dialog id="my-dialog"
data-hui-dialog-enter="transition duration-200 ease-out"
data-hui-dialog-enter-from="opacity-0 scale-95"
data-hui-dialog-enter-to="opacity-100 scale-100"
data-hui-dialog-leave="transition duration-150 ease-in"
data-hui-dialog-leave-from="opacity-100 scale-100"
data-hui-dialog-leave-to="opacity-0 scale-95">
<x-hui::dialog.background class="bg-black/50"/>
<x-hui::dialog.panel max-width="md">
...
</x-hui::dialog.panel>
</x-hui::dialog>
[!NOTE] The transition attributes can also be placed on individual child elements (background, panel, etc.) instead of the dialog itself. This allows different animations per element — for example, a fade on the background and a scale on the panel.
[!NOTE] During a leave transition, the dialog stays open until all transitions complete. Opens and closes are blocked while a transition is in progress.
The dialog renders as a native <dialog> with all default styles reset (no padding, no border, transparent background). The background is a fixed full-screen <div>. Style everything with your own classes.
<x-hui::dialog id="my-dialog">
<x-hui::dialog.background class="bg-black/50"/>
<x-hui::dialog.panel class="mx-auto mt-20 max-w-lg rounded-xl bg-white p-6 shadow-xl">
<x-hui::dialog.title class="text-lg font-semibold">Title</x-hui::dialog.title>
<x-hui::dialog.description class="mt-2 text-sm text-zinc-600">Description</x-hui::dialog.description>
<div class="mt-4 flex justify-end gap-2">
<button data-hui-dialog-close class="rounded px-3 py-1.5 text-sm text-zinc-600 hover:bg-zinc-100">Cancel</button>
<button data-hui-dialog-close class="rounded bg-red-600 px-3 py-1.5 text-sm text-white hover:bg-red-700">Delete</button>
</div>
</x-hui::dialog.panel>
</x-hui::dialog>
[!NOTE] The native
<dialog>element renders in the browser's top layer, so there are no z-index issues with other content.
| Prop | Type | Default | Description |
|---|---|---|---|
class |
string |
"" |
Custom classes for the dialog. |
open |
boolean |
false |
Opens the dialog on page load. |
close-on-escape |
boolean |
true |
Whether pressing Escape closes the dialog. |
close-on-backdrop-click |
boolean |
true |
Whether clicking the backdrop closes the dialog. |
scroll-lock |
boolean |
false |
Locks body scroll while the dialog is open. |
[!NOTE] Renders a native
<dialog/>element. Requires anidattribute for trigger binding and JS API. Allows all valid HTML<dialog/>attributes.
| Prop | Type | Default | Description |
|---|---|---|---|
class |
string |
"" |
Custom classes for the background. |
[!NOTE] Renders a full-screen
<div/>witharia-hidden="true". Use it to add a backdrop behind the dialog panel (e.g.class="bg-black/50"). Clicking the background area (outside the panel) closes the dialog.
| Prop | Type | Default | Description |
|---|---|---|---|
class |
string |
"" |
Custom classes for the overlay. |
[!NOTE] Renders a
<div/>witharia-hidden="true". Behaves the same asbackground— use whichever name fits your mental model.
| Prop | Type | Default | Description |
|---|---|---|---|
class |
string |
"" |
Custom classes for the panel. |
max-width |
string |
"" |
Panel max-width. One of: sm, md, lg, xl, 2xl–7xl, or full. |
[!NOTE] Allows all valid HTML
<div/>attributes (class, style, data-*, etc.).
| Prop | Type | Default | Description |
|---|---|---|---|
class |
string |
"" |
Custom classes for the title. |
[!NOTE] Renders an
<h2/>. Automatically linked to the dialog viaaria-labelledby.
| Prop | Type | Default | Description |
|---|---|---|---|
class |
string |
"" |
Custom classes for the description. |
[!NOTE] Renders a
<p/>. Automatically linked to the dialog viaaria-describedby.
| Key | Action |
|---|---|
Escape |
Close the dialog |
Tab |
Cycle focus within the dialog |
Shift+Tab |
Cycle focus backwards |
<dialog> element which provides role="dialog" and aria-modal="true" automatically.aria-labelledby is set automatically from dialog.title.aria-describedby is set automatically from dialog.description.aria-hidden="true".How can I help you explore Laravel packages today?