Skip to content
Praesepe UI

overlays

Modal

Accessible dialog overlay with focus trap, body teleport, Escape/backdrop dismiss, and confirm variant.

Copied!

Examples

Basic dialog

Inline trigger slot with default actions.

Copied!
<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.

Copied!
<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.

Copied!
<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

Component props
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

Talk to the studio

WhatsApp: TODO Email the studio Visit Praesepe