Skip to content
HyperUX Experimental
Demo

Focus-trapped modal dialog with Escape handling, optional backdrop, and scoped open/close events.


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:

Internal helper methods are private implementation details and are not part of the supported API contract.

Options

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.

Demo

Focus-trapped slide-over panel built with huxDialog, using CSS transitions for the slide animation and edge positioning.


<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

Error Handling

Accessibility Notes

Notes