Skip to content
data-slotv1.0.0
Esc
↑↓navigate↵open⌘Jpreview
On this page

Alert Dialog

A modal that asks for a decision before continuing with an important action.

@data-slot/alert-dialogSource ↗
bun add @data-slot/alert-dialog
npm install @data-slot/alert-dialog
pnpm add @data-slot/alert-dialog

Anatomy

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

Preview
Show codeHide 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>

Require an explicit choice

Escape will not dismiss this one — data-close-on-escape="false" forces a choice between Cancel and Continue.

Preview
Show codeHide 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>

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 element
  • alert-dialog-trigger - Element that toggles the alert dialog
  • alert-dialog-portal - Optional element portaled to document.body
  • alert-dialog-overlay - Required overlay
  • alert-dialog-content - Required modal content
  • alert-dialog-title - Title used for aria-labelledby
  • alert-dialog-description - Description used for aria-describedby
  • alert-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 content
  • data-open / data-closed on root, portal, overlay, and content
  • data-stack-index on 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 content
  • aria-modal="true" on content
  • aria-labelledby / aria-describedby wiring from title and description
  • Focus is trapped while open
  • Focus returns to the trigger or previously focused element when closed