overlays
Modal
Accessible dialog overlay with focus trap, body teleport, Escape/backdrop dismiss, and confirm variant.
Examples
Basic dialog
Inline trigger slot with default actions.
Project details
Review invoice INV-2026-089 for Toko Maju Jaya before sending.
Customer: Toko Maju Jaya · Bandung. Amount due: Rp 2.450.000 (IDR).
<x-ui.modal id="demo-basic" title="Project details" description="Review invoice INV-2026-089 for Toko Maju Jaya before sending.">
<x-slot:trigger>
<x-ui.button>Open dialog</x-ui.button>
</x-slot:trigger>
<p>Customer: Toko Maju Jaya · Bandung. Amount due: Rp 2.450.000 (IDR).</p>
</x-ui.modal>
<div x-data="{ open: false }">
<button
type="button"
class="inline-flex h-9 items-center justify-center gap-2 rounded-control bg-brand px-3.5 text-sm font-medium text-on-brand shadow-[inset_0_1px_0_0_rgba(255,255,255,0.16),0_1px_2px_0_rgba(0,0,0,0.06)] transition duration-150 hover:opacity-90 motion-safe:active:translate-y-[0.5px]"
x-on:click="open = true"
>
Open dialog
</button>
<template x-teleport="body">
<div
x-show="open"
x-cloak
class="fixed inset-0 z-50 flex items-end justify-center p-4 sm:items-center"
role="dialog"
aria-modal="true"
aria-labelledby="demo-modal-title"
x-trap.inert.noscroll="open"
x-on:keydown.escape.window="if (open) open = false"
>
<div
class="absolute inset-0 bg-overlay backdrop-blur-[2px]"
x-show="open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-100"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
x-on:click="open = false"
></div>
<div
class="relative z-10 w-full max-w-lg rounded-card border border-border bg-surface-raised p-5 shadow-lg ring-1 ring-black/5"
x-show="open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 scale-95 translate-y-1"
x-transition:enter-end="opacity-100 scale-100 translate-y-0"
x-transition:leave="transition ease-in duration-100"
x-transition:leave-start="opacity-100 scale-100 translate-y-0"
x-transition:leave-end="opacity-0 scale-95 translate-y-1"
x-on:click.stop
>
<h2 id="demo-modal-title" class="text-lg font-semibold tracking-tight text-fg">Project details</h2>
<p class="mt-2 text-sm text-fg-muted leading-relaxed">Review invoice INV-2026-089 for Toko Maju Jaya before sending.</p>
<div class="mt-6 flex justify-end gap-2">
<button type="button" class="inline-flex h-9 items-center rounded-control border border-border bg-surface-sunken px-3.5 text-sm shadow-2xs" x-on:click="open = false">Close</button>
</div>
</div>
</div>
</template>
</div>
Confirm variant
Destructive confirm pattern with danger treatment.
Approve payout?
Release Rp 18.750.000 to CV Nusantara for PO-8841. This cannot be undone.
<x-ui.modal
id="demo-confirm"
variant="confirm"
title="Approve payout?"
description="Release Rp 18.750.000 to CV Nusantara for PO-8841. This cannot be undone."
>
<x-slot:trigger>
<x-ui.button variant="danger">Approve payout</x-ui.button>
</x-slot:trigger>
<x-slot:action>
<x-ui.button variant="danger" x-on:click="hide()">Approve</x-ui.button>
</x-slot:action>
</x-ui.modal>
<div x-data="{ open: false }">
<button
type="button"
class="inline-flex h-9 items-center justify-center gap-2 rounded-control bg-danger px-3.5 text-sm font-medium text-on-danger shadow-[inset_0_1px_0_0_rgba(255,255,255,0.16),0_1px_2px_0_rgba(0,0,0,0.06)] transition duration-150 hover:opacity-90 motion-safe:active:translate-y-[0.5px]"
x-on:click="open = true"
>
Approve payout
</button>
<template x-teleport="body">
<div
x-show="open"
x-cloak
class="fixed inset-0 z-50 flex items-end justify-center p-4 sm:items-center"
role="dialog"
aria-modal="true"
aria-labelledby="confirm-modal-title"
x-trap.inert.noscroll="open"
x-on:keydown.escape.window="if (open) open = false"
>
<div
class="absolute inset-0 bg-overlay backdrop-blur-[2px]"
x-show="open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-100"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
x-on:click="open = false"
></div>
<div
class="relative z-10 w-full max-w-lg rounded-card border border-border bg-surface-raised p-5 shadow-lg ring-1 ring-black/5"
x-show="open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 scale-95 translate-y-1"
x-transition:enter-end="opacity-100 scale-100 translate-y-0"
x-transition:leave="transition ease-in duration-100"
x-transition:leave-start="opacity-100 scale-100 translate-y-0"
x-transition:leave-end="opacity-0 scale-95 translate-y-1"
x-on:click.stop
>
<h2 id="confirm-modal-title" class="text-lg font-semibold tracking-tight text-fg">Approve payout?</h2>
<p class="mt-2 text-sm text-fg-muted leading-relaxed">Release Rp 18.750.000 to CV Nusantara for PO-8841. This cannot be undone.</p>
<div class="mt-6 flex justify-end gap-2">
<button type="button" class="inline-flex h-9 items-center rounded-control border border-border bg-surface-sunken px-3.5 text-sm shadow-2xs" x-on:click="open = false">Cancel</button>
<button type="button" class="inline-flex h-9 items-center rounded-control bg-danger px-3.5 text-sm font-medium text-on-danger shadow-[inset_0_1px_0_0_rgba(255,255,255,0.16),0_1px_2px_0_rgba(0,0,0,0.06)]" x-on:click="open = false">Approve</button>
</div>
</div>
</div>
</template>
</div>
Open via window event
Dispatch ui-modal-open with a matching id.
Approval queue
Open INV-2026-089 from the finance toolbar without nesting the trigger.
Listen for the ui-modal-open window event and match detail.id.
<div class="flex flex-wrap items-center gap-3">
<x-ui.button
variant="outline"
x-on:click="window.dispatchEvent(new CustomEvent('ui-modal-open', { detail: { id: 'demo-event' } }))"
>
Open via event
</x-ui.button>
<x-ui.modal
id="demo-event"
title="Approval queue"
description="Open INV-2026-089 from the finance toolbar without nesting the trigger."
>
<p>Listen for the ui-modal-open window event and match detail.id.</p>
</x-ui.modal>
</div>
<div>
<button
type="button"
class="inline-flex h-9 items-center justify-center gap-2 rounded-control border border-border-strong bg-transparent px-3.5 text-sm font-medium text-fg transition duration-150 hover:bg-surface-sunken"
x-on:click="window.dispatchEvent(new CustomEvent('ui-modal-open', { detail: { id: 'demo-event' } }))"
>
Open via event
</button>
</div>
Props API
| Prop | Type | Default | Description |
|---|---|---|---|
| id | string | auto | Dialog id used for window event open/close targeting. |
| title | string | Dialog | Accessible dialog title referenced by aria-labelledby. |
| description | string|null | null | Optional description referenced by aria-describedby. |
| size | sm|md|lg|xl | md | Max width of the dialog panel. |
| variant | default|confirm | default | Confirm variant shows a danger icon treatment. |
| staticBackdrop | bool | false | When true, backdrop clicks do not close the dialog. |
| open | bool | false | Initial open state for controlled demos. |
Accessibility notes
Architected with WCAG 2.2 AA patterns in mind. These notes document the exact keyboard and ARIA behavior for this component.
- Uses role="dialog", aria-modal="true", aria-labelledby, and optional aria-describedby.
- Focus is trapped with Alpine x-trap.inert.noscroll while open. Body scroll is locked.
- Dialog content teleports to body to avoid clipping inside overflow-hidden preview containers.
- Escape closes the dialog. Backdrop click closes unless staticBackdrop is true.
- Focus returns to the trigger element after close.
- Open via trigger slot click or window event: window.dispatchEvent(new CustomEvent("ui-modal-open", { detail: { id } })).
Built by Praesepe
Architecture & custom software partner
Praesepe uses these exact UI primitives to ship SME dashboards, inventory systems, invoicing desks, and internal tools faster, without rebuilding a design system for every client.
- Same Blade + Tailwind 4 + Alpine 3 stack you see in this docs site
- Opinionated for Indonesian SME ops: invoices, stock, roles, approvals
- Architected with WCAG 2.2 AA patterns in mind for keyboard and ARIA