Focus-trapped modal dialog with Escape handling, optional backdrop, and scoped open/close events.
Delete item
Are you sure you want to delete this item? This action cannot be undone.
Alpine.js Dialog
huxDialog handles open/close state, optional backdrop behavior, and scoped lifecycle events for modal UI in Alpine.js. Pair it with your own dialog markup and focus trap directives.
This pattern expects the Alpine Focus plugin when you use x-trap in your markup: @alpinejs/focus.
API
huxDialog(config)
Returns an Alpine data object with:
isOpen: booleandialogId: string | nullisPersistent: booleanisSeamless: booleandialogTitleId: string | nullcloseOnBackdrop: booleancloseOnEscape: booleanshowBackdrop: booleanopenDialog(): voidtoggleDialog(): voidcloseDialog(): voidhandleBackdropClick(): voidhandleEscape(): void
Internal helper methods are private implementation details and are not part of the supported API contract.
Options
dialogId: string(optional)startsOpen: boolean(default:false)isPersistent: boolean(default:false)isSeamless: boolean(default:false)
Quick Start
<div x-data="huxDialog({ dialogId: 'settings' })">
<button type="button" x-on:click="openDialog()">Open dialog</button>
<template x-teleport="body">
<div x-cloak x-show="isOpen" x-on:keydown.escape.stop="handleEscape()">
<div
x-cloak
x-show="showBackdrop"
aria-hidden="true"
x-on:click="handleBackdropClick()"
></div>
<div
x-trap.noscroll="isOpen"
role="dialog"
aria-modal="true"
x-bind:aria-labelledby="dialogTitleId || null"
>
<h2 x-bind:id="dialogTitleId || null">Settings</h2>
<button type="button" x-on:click="closeDialog()">Close</button>
</div>
</div>
</template>
</div>Common Usage Patterns
Persistent Confirmation Dialog
huxDialog({
dialogId: 'confirm-delete',
isPersistent: true,
})Seamless Dialog (No Backdrop)
huxDialog({
dialogId: 'quick-actions',
isSeamless: true,
})Slide-over Panel
huxDialog can drive a slide-over panel — a full-height drawer that slides in from a screen edge. No additional component is needed; the slide animation and positioning are handled entirely with CSS transition classes.
Focus-trapped slide-over panel built with huxDialog, using CSS transitions for
the slide animation and edge positioning.
User Settings
Profile
Preferences
<div x-data="huxDialog({ dialogId: 'settings' })">
<button type="button" x-on:click="openDialog()">Open</button>
<template x-teleport="body">
<div
x-cloak
x-show="isOpen"
class="fixed inset-0 z-50"
x-on:keydown.escape.stop="handleEscape()"
>
<!-- Backdrop -->
<template x-if="showBackdrop">
<div
x-cloak
x-show="isOpen"
x-transition.opacity
class="fixed inset-0 bg-gray-900/60"
aria-hidden="true"
x-on:click="handleBackdropClick()"
></div>
</template>
<!-- Panel — slides in from the right -->
<div
x-cloak
x-show="isOpen"
x-trap.noscroll="isOpen"
x-transition:enter="transform transition-transform duration-300 ease-out"
x-transition:enter-start="translate-x-full"
x-transition:enter-end="translate-x-0"
x-transition:leave="transform transition-transform duration-200 ease-in"
x-transition:leave-start="translate-x-0"
x-transition:leave-end="translate-x-full"
role="dialog"
aria-modal="true"
x-bind:aria-labelledby="dialogTitleId || null"
class="fixed top-0 right-0 h-full w-full max-w-md bg-white shadow-xl"
>
<h2 x-bind:id="dialogTitleId || null">Settings</h2>
<button type="button" x-on:click="closeDialog()">Close</button>
</div>
</div>
</template>
</div>For a left-side slide-over, position the panel with left-0 and swap the enter/leave translate classes to -translate-x-full.
Behavior Contract
init()derivesdialogTitleIdfromdialogIdas${dialogId}-title, otherwisenull.openDialog()setsisOpen = trueand dispatches open event(s).closeDialog()setsisOpen = falseand dispatches close event(s).toggleDialog()flips open state by delegating toopenDialog()andcloseDialog().isPersistentdisables both Escape and backdrop close via computed flags.isSeamlesshides backdrop viashowBackdrop.- Event names are scoped when
dialogIdexists.
Error Handling
- Dialog methods are defensive and do not throw for repeated calls (
openDialog()while open,closeDialog()while closed). - Unknown event listeners or missing external handlers do not block internal state transitions.
Accessibility Notes
- Keep modal container with
role="dialog"andaria-modal="true". - Provide a visible or screen-reader title and wire
aria-labelledbyto that element. - Keep close controls as labeled
buttonelements withtype="button". - Use keyboard escape handling (
x-on:keydown.escape.stop) and focus trapping (x-trap) so keyboard users remain inside the modal while open.
Notes
- Events emitted:
hux-dialog:{dialogId}:openhux-dialog:{dialogId}:closehux-dialog:openandhux-dialog:closewhendialogIdis missing