Alert Dialog
A modal that asks for a decision before continuing with an important action.
@data-slot/alert-dialogSource ↗bun add @data-slot/alert-dialognpm install @data-slot/alert-dialogpnpm add @data-slot/alert-dialogAnatomy
The overlay is required. The portal is optional and gets moved to document.body while the dialog is open. alert-dialog-action does not close the dialog on its own — wire it to your own handler.
<div data-slot="alert-dialog">
<button data-slot="alert-dialog-trigger">Delete project</button>
<div data-slot="alert-dialog-portal">
<div data-slot="alert-dialog-overlay" hidden></div>
<div data-slot="alert-dialog-content" hidden>
<div data-slot="alert-dialog-header">
<div data-slot="alert-dialog-media">!</div>
<h2 data-slot="alert-dialog-title">Delete project?</h2>
<p data-slot="alert-dialog-description">
This action cannot be undone.
</p>
</div>
<div data-slot="alert-dialog-footer">
<button data-slot="alert-dialog-cancel">Cancel</button>
<button data-slot="alert-dialog-action">Delete</button>
</div>
</div>
</div>
</div>
Examples
Confirm an action
Delete project?
This will permanently remove your production config and API keys.
Delete project?
This will permanently remove your production config and API keys.
Show code
<div data-slot="alert-dialog" class="alert-dialog-root">
<button data-slot="alert-dialog-trigger" class="alert-dialog-trigger">
Delete Project
</button>
<div data-slot="alert-dialog-portal">
<div data-slot="alert-dialog-overlay" class="alert-dialog-overlay" hidden></div>
<div data-slot="alert-dialog-content" class="alert-dialog-panel" hidden>
<div data-slot="alert-dialog-header" class="alert-dialog-header">
<div data-slot="alert-dialog-media" class="alert-dialog-media">!</div>
<h2 data-slot="alert-dialog-title" class="alert-dialog-title">Delete project?</h2>
<p data-slot="alert-dialog-description" class="alert-dialog-description">
This will permanently remove your production config and API keys.
</p>
</div>
<div data-slot="alert-dialog-footer" class="alert-dialog-footer">
<button data-slot="alert-dialog-cancel" class="alert-dialog-cancel">Cancel</button>
<button
data-slot="alert-dialog-action"
data-demo-alert-confirm
class="alert-dialog-action"
>
Delete
</button>
</div>
</div>
</div>
</div>
<style>
.alert-dialog-trigger {
padding: 0.5rem 1rem;
background: var(--surface);
color: var(--text);
border: 1px solid var(--border);
cursor: pointer;
transition: opacity 0.15s cubic-bezier(0.4, 0, 0.2, 1);
}
.alert-dialog-trigger:hover { opacity: 0.9; }
.alert-dialog-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 100;
opacity: 0;
transition: opacity 0.2s ease;
}
.alert-dialog-overlay[data-open] { opacity: 1; }
.alert-dialog-overlay[data-starting-style],
.alert-dialog-overlay[data-ending-style] { opacity: 0; }
.alert-dialog-panel {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.95);
background: var(--surface);
padding: 2rem;
max-width: 28rem;
width: 91.666667%;
border: 1px solid var(--border);
z-index: 101;
opacity: 0;
transition:
opacity 0.2s ease,
transform 0.2s ease;
}
.alert-dialog-panel[data-open] {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
.alert-dialog-panel[data-starting-style],
.alert-dialog-panel[data-ending-style] {
opacity: 0;
transform: translate(-50%, -50%) scale(0.95);
}
.alert-dialog-header {
display: grid;
justify-items: center;
gap: 0.5rem;
text-align: center;
margin-bottom: 1.5rem;
}
.alert-dialog-media {
width: 2rem;
height: 2rem;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
background: var(--code-bg);
border: 1px solid var(--border);
color: var(--text);
font-weight: 700;
}
.alert-dialog-title {
margin: 0;
font-weight: 700;
}
.alert-dialog-description {
margin: 0;
color: var(--muted);
line-height: 1.25rem;
}
.alert-dialog-footer {
display: flex;
flex-direction: column-reverse;
gap: 0.5rem;
}
.alert-dialog-cancel,
.alert-dialog-action {
padding: 0.375rem 0.75rem;
cursor: pointer;
transition: opacity 0.15s cubic-bezier(0.4, 0, 0.2, 1);
}
.alert-dialog-action:hover { opacity: 0.9; }
.alert-dialog-cancel {
background: none;
border: 1px solid var(--border);
transition: background-color 0.15s cubic-bezier(0.4, 0, 0.2, 1);
}
.alert-dialog-cancel:hover { background: var(--code-bg); }
.alert-dialog-action {
background: var(--text);
color: var(--bg);
border: none;
transition: opacity 0.15s cubic-bezier(0.4, 0, 0.2, 1);
}
.alert-dialog-action:hover { opacity: 0.9; }
@media (min-width: 640px) {
.alert-dialog-footer {
flex-direction: row;
justify-content: flex-end;
}
}
</style><div data-slot="alert-dialog">
<button
data-slot="alert-dialog-trigger"
class="px-4 py-2 bg-[var(--surface)] text-text border border-[var(--border)] cursor-pointer
hover:opacity-90 transition-opacity"
>
Delete Project
</button>
<div data-slot="alert-dialog-portal">
<div
data-slot="alert-dialog-overlay"
class="fixed inset-0 bg-black/50 z-50 opacity-0 transition-opacity duration-200
data-open:opacity-100
data-starting-style:opacity-0
data-ending-style:opacity-0"
hidden
></div>
<div
data-slot="alert-dialog-content"
class="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2
bg-[var(--surface)] p-8 max-w-md w-11/12 border border-[var(--border)] z-50
opacity-0 scale-95 transition-all duration-200
data-open:opacity-100
data-open:scale-100
data-starting-style:opacity-0
data-starting-style:scale-95
data-ending-style:opacity-0
data-ending-style:scale-95"
hidden
>
<div
data-slot="alert-dialog-header"
class="grid justify-items-center gap-2 text-center mb-6"
>
<div
data-slot="alert-dialog-media"
class="inline-flex size-8 items-center justify-center rounded-full border border-[var(--border)] bg-code-bg font-bold text-text"
>
!
</div>
<h2 data-slot="alert-dialog-title" class="font-bold mt-0 mb-0">
Delete project?
</h2>
<p data-slot="alert-dialog-description" class="text-[var(--muted)] m-0">
This will permanently remove your production config and API keys.
</p>
</div>
<div
data-slot="alert-dialog-footer"
class="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end"
>
<button
data-slot="alert-dialog-cancel"
class="px-3 py-1.5 bg-transparent border border-[var(--border)] cursor-pointer hover:bg-code-bg transition-colors"
>
Cancel
</button>
<button
data-slot="alert-dialog-action"
data-demo-alert-confirm
class="px-3 py-1.5 bg-text text-bg border-none cursor-pointer hover:opacity-90 transition-opacity"
>
Delete
</button>
</div>
</div>
</div>
</div>import { create } from "@data-slot/alert-dialog";
// Initialize after the markup is in the document.
const controllers = create();
// Clean up before removing the component.
// controllers.forEach((controller) => controller.destroy());
document.querySelectorAll('[data-slot="alert-dialog"]').forEach((root) => {
root.querySelectorAll('[data-demo-alert-confirm]').forEach((button) => {
button.addEventListener("click", () => {
root.dispatchEvent(new CustomEvent("alert-dialog:set", { detail: { open: false } }));
});
});
});Require an explicit choice
Escape will not dismiss this one — data-close-on-escape="false" forces a choice between Cancel and Continue.
Delete project?
This will permanently remove your production config and API keys.
Delete project?
This will permanently remove your production config and API keys.
Show code
<div data-slot="alert-dialog" data-close-on-escape="false" class="alert-dialog-root">
<button data-slot="alert-dialog-trigger" class="alert-dialog-trigger">
Delete Project
</button>
<div data-slot="alert-dialog-portal">
<div data-slot="alert-dialog-overlay" class="alert-dialog-overlay" hidden></div>
<div data-slot="alert-dialog-content" class="alert-dialog-panel" hidden>
<div data-slot="alert-dialog-header" class="alert-dialog-header">
<div data-slot="alert-dialog-media" class="alert-dialog-media">!</div>
<h2 data-slot="alert-dialog-title" class="alert-dialog-title">Delete project?</h2>
<p data-slot="alert-dialog-description" class="alert-dialog-description">
This will permanently remove your production config and API keys.
</p>
</div>
<div data-slot="alert-dialog-footer" class="alert-dialog-footer">
<button data-slot="alert-dialog-cancel" class="alert-dialog-cancel">Cancel</button>
<button
data-slot="alert-dialog-action"
data-demo-alert-confirm
class="alert-dialog-action"
>
Delete
</button>
</div>
</div>
</div>
</div>
<style>
.alert-dialog-trigger {
padding: 0.5rem 1rem;
background: var(--surface);
color: var(--text);
border: 1px solid var(--border);
cursor: pointer;
transition: opacity 0.15s cubic-bezier(0.4, 0, 0.2, 1);
}
.alert-dialog-trigger:hover { opacity: 0.9; }
.alert-dialog-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 100;
opacity: 0;
transition: opacity 0.2s ease;
}
.alert-dialog-overlay[data-open] { opacity: 1; }
.alert-dialog-overlay[data-starting-style],
.alert-dialog-overlay[data-ending-style] { opacity: 0; }
.alert-dialog-panel {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.95);
background: var(--surface);
padding: 2rem;
max-width: 28rem;
width: 91.666667%;
border: 1px solid var(--border);
z-index: 101;
opacity: 0;
transition:
opacity 0.2s ease,
transform 0.2s ease;
}
.alert-dialog-panel[data-open] {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
.alert-dialog-panel[data-starting-style],
.alert-dialog-panel[data-ending-style] {
opacity: 0;
transform: translate(-50%, -50%) scale(0.95);
}
.alert-dialog-header {
display: grid;
justify-items: center;
gap: 0.5rem;
text-align: center;
margin-bottom: 1.5rem;
}
.alert-dialog-media {
width: 2rem;
height: 2rem;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
background: var(--code-bg);
border: 1px solid var(--border);
color: var(--text);
font-weight: 700;
}
.alert-dialog-title {
margin: 0;
font-weight: 700;
}
.alert-dialog-description {
margin: 0;
color: var(--muted);
line-height: 1.25rem;
}
.alert-dialog-footer {
display: flex;
flex-direction: column-reverse;
gap: 0.5rem;
}
.alert-dialog-cancel,
.alert-dialog-action {
padding: 0.375rem 0.75rem;
cursor: pointer;
transition: opacity 0.15s cubic-bezier(0.4, 0, 0.2, 1);
}
.alert-dialog-action:hover { opacity: 0.9; }
.alert-dialog-cancel {
background: none;
border: 1px solid var(--border);
transition: background-color 0.15s cubic-bezier(0.4, 0, 0.2, 1);
}
.alert-dialog-cancel:hover { background: var(--code-bg); }
.alert-dialog-action {
background: var(--text);
color: var(--bg);
border: none;
transition: opacity 0.15s cubic-bezier(0.4, 0, 0.2, 1);
}
.alert-dialog-action:hover { opacity: 0.9; }
@media (min-width: 640px) {
.alert-dialog-footer {
flex-direction: row;
justify-content: flex-end;
}
}
</style><div data-slot="alert-dialog" data-close-on-escape="false">
<button
data-slot="alert-dialog-trigger"
class="px-4 py-2 bg-[var(--surface)] text-text border border-[var(--border)] cursor-pointer
hover:opacity-90 transition-opacity"
>
Delete Project
</button>
<div data-slot="alert-dialog-portal">
<div
data-slot="alert-dialog-overlay"
class="fixed inset-0 bg-black/50 z-50 opacity-0 transition-opacity duration-200
data-open:opacity-100
data-starting-style:opacity-0
data-ending-style:opacity-0"
hidden
></div>
<div
data-slot="alert-dialog-content"
class="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2
bg-[var(--surface)] p-8 max-w-md w-11/12 border border-[var(--border)] z-50
opacity-0 scale-95 transition-all duration-200
data-open:opacity-100
data-open:scale-100
data-starting-style:opacity-0
data-starting-style:scale-95
data-ending-style:opacity-0
data-ending-style:scale-95"
hidden
>
<div
data-slot="alert-dialog-header"
class="grid justify-items-center gap-2 text-center mb-6"
>
<div
data-slot="alert-dialog-media"
class="inline-flex size-8 items-center justify-center rounded-full border border-[var(--border)] bg-code-bg font-bold text-text"
>
!
</div>
<h2 data-slot="alert-dialog-title" class="font-bold mt-0 mb-0">
Delete project?
</h2>
<p data-slot="alert-dialog-description" class="text-[var(--muted)] m-0">
This will permanently remove your production config and API keys.
</p>
</div>
<div
data-slot="alert-dialog-footer"
class="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end"
>
<button
data-slot="alert-dialog-cancel"
class="px-3 py-1.5 bg-transparent border border-[var(--border)] cursor-pointer hover:bg-code-bg transition-colors"
>
Cancel
</button>
<button
data-slot="alert-dialog-action"
data-demo-alert-confirm
class="px-3 py-1.5 bg-text text-bg border-none cursor-pointer hover:opacity-90 transition-opacity"
>
Delete
</button>
</div>
</div>
</div>
</div>import { create } from "@data-slot/alert-dialog";
// Initialize after the markup is in the document.
const controllers = create();
// Clean up before removing the component.
// controllers.forEach((controller) => controller.destroy());
document.querySelectorAll('[data-slot="alert-dialog"]').forEach((root) => {
root.querySelectorAll('[data-demo-alert-confirm]').forEach((button) => {
button.addEventListener("click", () => {
root.dispatchEvent(new CustomEvent("alert-dialog:set", { detail: { open: false } }));
});
});
});API reference
Initialization
create(scope?)
Auto-discovers and binds all alert dialogs in a scope.
import { create } from "@data-slot/alert-dialog";
const controllers = create();
createAlertDialog(root, options?)
import { createAlertDialog } from "@data-slot/alert-dialog";
const alertDialog = createAlertDialog(element, {
closeOnClickOutside: false,
onOpenChange: (open) => console.log(open),
});
Slots
Runtime Slots
alert-dialog- Root elementalert-dialog-trigger- Element that toggles the alert dialogalert-dialog-portal- Optional element portaled todocument.bodyalert-dialog-overlay- Required overlayalert-dialog-content- Required modal contentalert-dialog-title- Title used foraria-labelledbyalert-dialog-description- Description used foraria-describedbyalert-dialog-cancel- Close button
Style-only Slots
alert-dialog-header- Optional layout wrapper for the title, description, and media.alert-dialog-footer- Optional layout wrapper for the action and cancel buttons.alert-dialog-media- Optional icon or illustration area in the header.alert-dialog-action- Confirmation action button; wire your own handler and close the dialog explicitly.
alert-dialog-action is intentionally just a styled action slot. It does not close automatically.
Data Attributes
Set these on the alert-dialog root. JavaScript options take precedence. Empty attributes or "true" enable a boolean; "false" disables it.
| Attribute | Type | Default | Description |
|---|---|---|---|
data-default-open |
boolean |
false |
Initial open state |
data-close-on-click-outside |
boolean |
false |
Close when clicking the overlay |
data-close-on-escape |
boolean |
true |
Close on Escape |
data-lock-scroll |
boolean |
true |
Lock page scroll while open |
State Attributes
data-state="open" | "closed"on root, portal, overlay, and contentdata-open/data-closedon root, portal, overlay, and contentdata-stack-indexon overlay and content when multiple modal layers are open
Options
| Option | Type | Default | Description |
|---|---|---|---|
defaultOpen |
boolean |
false |
Initial open state |
onOpenChange |
(open: boolean) => void |
undefined |
Called when open state changes |
closeOnClickOutside |
boolean |
false |
Close when clicking the overlay |
closeOnEscape |
boolean |
true |
Close when pressing Escape |
lockScroll |
boolean |
true |
Lock page scroll while open |
Controller
| Method | Description |
|---|---|
open() |
Open the alert dialog |
close() |
Close the alert dialog |
toggle() |
Toggle the alert dialog |
destroy() |
Remove listeners and cleanup |
isOpen |
Current open state |
Controller Destruction
destroy() permanently disposes the controller and hides any open surface without
emitting an additional change event. Repeated destruction is safe; methods on the
old controller become no-ops. Create a new controller on the same root to rebind it.
Destruction restores prior focus, falling back to a surviving trigger if the prior target was removed. Destroying an unopened modal does not move focus.
Events
| Event | Detail | Description |
|---|---|---|
alert-dialog:change |
{ open: boolean } |
Fired when open state changes |
alert-dialog:set |
{ open: boolean } |
Programmatically set open state |
Accessibility
role="alertdialog"on contentaria-modal="true"on contentaria-labelledby/aria-describedbywiring from title and description- Focus is trapped while open
- Focus returns to the trigger or previously focused element when closed