Alert Dialog
Free · MITHeadless, accessible, and styled entirely through the attribute surface below — bring your own CSS system, or start from the reference theme.
Install
Without a flag you get no styling at all — plain HTML carrying the attribute surface below. --theme copies the same theme in two dialects;--bridge lumos re-points either one onto Lumos for Astro's tokens (it needs a theme to re-point, and Lumos itself, already in your project).
Unstyled — behavior only, you write the CSS
npx astrocraft-ui add overlays/alert-dialogPlain-CSS theme — no build step
npx astrocraft-ui add overlays/alert-dialog --theme cssTailwind theme — needs Tailwind v4
npx astrocraft-ui add overlays/alert-dialog --theme tailwindPlain-CSS theme, re-pointed onto Lumos tokens
npx astrocraft-ui add overlays/alert-dialog --theme css --bridge lumosTailwind theme, re-pointed onto Lumos tokens
npx astrocraft-ui add overlays/alert-dialog --theme tailwind --bridge lumosLive demo
AlertDialog
Click the backdrop: nothing happens, which is the whole difference from Dialog. Escape still cancels, and focus lands on Keep it rather than on the destructive button — so the key you reach for to make an interruption go away does not delete anything.
Attribute surface
With no class-merging machinery, these attributes are the whole extension point. State is a data-attribute or a native attribute; visibility is the hiddenattribute. Generated from source by pnpm slots.
| Component | Slots | Variants | Runtime state | Native state |
|---|---|---|---|---|
| `AlertDialog.astro` | `alert-dialog` | — | — | — |
| `AlertDialogAction.astro` | `alert-dialog-action` | `data-variant`: `primary` · `secondary` · `outline` · `ghost` · `destructive` `data-size`: `sm` · `md` · `lg` | — | — |
| `AlertDialogCancel.astro` | `alert-dialog-cancel` | `data-variant`: `primary` · `secondary` · `outline` · `ghost` · `destructive` `data-size`: `sm` · `md` · `lg` | — | — |
| `AlertDialogDescription.astro` | `alert-dialog-description` | — | — | — |
| `AlertDialogTitle.astro` | `alert-dialog-title` | — | — | — |
Source
What the command copies — 6 files, alias-free so the copy lands standing alone. This is the whole component; there is nothing else.
---
// src/components/ui/overlays/alert-dialog/AlertDialog.astro — headless primitive (see ../../README.md).
// A confirmation that interrupts: "Delete this project?". Open it with a DialogTrigger whose `for`
// matches this dialog's `id` (re-exported as AlertDialogTrigger), and close it with an
// AlertDialogAction or AlertDialogCancel. Compose AlertDialogTitle / AlertDialogDescription inside
// and point `aria-labelledby` / `aria-describedby` at their ids.
//
// THIS IS NOT A STYLED DIALOG. Two things differ, and both are behavior:
//
// 1. `role="alertdialog"` changes how assistive tech announces it — the description is read
// immediately on open rather than waiting to be explored, because the user is being asked to
// decide something now. That is the entire reason the role exists.
// 2. It does not light-dismiss. The shared `_dialog` controller closes any <dialog> on a backdrop
// click, which is right for a Dialog and wrong here: a stray click outside a destructive
// confirmation should not silently answer it. The four-line capture listener below is how that
// is switched off WITHOUT touching `_dialog.ts`, which is frozen byte-identical to the theme
// repo (see AGENTS.md) — a click on the backdrop reports the <dialog> itself as its target, so
// stopping such a click at the element keeps it from ever reaching the document-level listener.
//
// Escape still closes it, deliberately: Escape means cancel, the same answer AlertDialogCancel gives,
// and taking it away leaves a keyboard user with an interruption they cannot dismiss. Transitions,
// scrim and scroll-lock come from `_overlay.css`.
import "../../_overlay.css";
import type { HTMLAttributes } from "astro/types";
type Props = HTMLAttributes<"dialog">;
const { class: className, ...rest } = Astro.props;
---
<dialog role="alertdialog" class={className} data-slot="alert-dialog" {...rest}>
<slot />
</dialog>
<script>
import "../../_dialog";
import { onReadyOnce } from "../../_once";
function wire(root: HTMLElement) {
root.addEventListener(
"click",
(event) => {
// Only the backdrop: a click on the panel or on any control inside it reports that element,
// not the <dialog>, so everything within keeps working normally.
if (event.target === root) event.stopPropagation();
},
true,
);
}
onReadyOnce('[data-slot="alert-dialog"]', wire);
</script>
---
// src/components/ui/overlays/alert-dialog/AlertDialogAction.astro — AlertDialog part (see ../../README.md).
// The button that goes through with it. Closes the dialog via the shared `_dialog` controller's
// data-dialog-close hook; attach your own click handler for the work itself, which runs first
// because the controller is delegated on `document` and only sees the click on the way up.
//
// It is NOT focused on open — AlertDialogCancel is. Confirming should take a deliberate act.
import type { HTMLAttributes } from "astro/types";
type Props = HTMLAttributes<"button"> & {
variant?: "primary" | "secondary" | "outline" | "ghost" | "destructive";
size?: "sm" | "md" | "lg";
};
const { variant = "primary", size = "md", class: className, ...rest } = Astro.props;
---
<button
type="button"
data-dialog-close
data-slot="alert-dialog-action"
data-variant={variant}
data-size={size}
class={className}
{...rest}
>
<slot />
</button>
---
// src/components/ui/overlays/alert-dialog/AlertDialogCancel.astro — AlertDialog part (see ../../README.md).
// The way out. Closes the dialog via the shared `_dialog` controller's data-dialog-close hook.
//
// `autofocus` is ON BY DEFAULT, which is the one opinion this component holds: a modal <dialog>
// otherwise focuses the first focusable thing it contains, and if that happens to be the destructive
// action then Enter — the key someone reaches for to make an interruption go away — carries it out.
// Focusing the least destructive choice is the ARIA alert-dialog recommendation for exactly that
// reason. Pass `autofocus={false}` if you have somewhere better to put the caret.
// jsx-a11y is right about autofocus on a PAGE: it moves the caret out from under someone who never
// asked for it. Inside a modal <dialog> that has just opened, focus is being moved regardless and
// the only question is where to — and the ARIA alert-dialog pattern answers "the least destructive
// action", which is this button. Disabled for the file because the file exists to render it.
/* eslint-disable astro/jsx-a11y/no-autofocus */
import type { HTMLAttributes } from "astro/types";
type Props = HTMLAttributes<"button"> & {
variant?: "primary" | "secondary" | "outline" | "ghost" | "destructive";
size?: "sm" | "md" | "lg";
};
const {
variant = "outline",
size = "md",
autofocus = true,
class: className,
...rest
} = Astro.props;
---
{
/* The rule is right about autofocus on a PAGE — it moves the caret out from under someone who never
asked. Inside a modal <dialog> that has just opened, focus is already being moved and the only
question is where to; the ARIA alert-dialog pattern answers "the least destructive action", which
is this button. See the component comment above. */
}
{}
<button
type="button"
autofocus={autofocus}
data-dialog-close
data-slot="alert-dialog-cancel"
data-variant={variant}
data-size={size}
class={className}
{...rest}
>
<slot />
</button>
---
// src/components/ui/overlays/alert-dialog/AlertDialogDescription.astro — AlertDialog part (see ../../README.md).
// Give it an `id` and point the AlertDialog's `aria-describedby` at it. Say what the consequence is
// and whether it can be undone; this text is announced on open, so it is the one chance to say so.
import type { HTMLAttributes } from "astro/types";
type Props = HTMLAttributes<"p">;
const { class: className, ...rest } = Astro.props;
---
<p class={className} data-slot="alert-dialog-description" {...rest}><slot /></p>
---
// src/components/ui/overlays/alert-dialog/AlertDialogTitle.astro — AlertDialog part (see ../../README.md).
// Give it an `id` and point the AlertDialog's `aria-labelledby` at it. Phrase it as the question
// being asked — with `role="alertdialog"` this is the first thing read aloud, and "Are you sure?"
// tells a screen-reader user nothing about what they are being sure of.
import type { HTMLAttributes } from "astro/types";
type Props = HTMLAttributes<"h2">;
const { class: className, ...rest } = Astro.props;
---
<h2 class={className} data-slot="alert-dialog-title" {...rest}><slot /></h2>
import { DialogTrigger as AlertDialogTrigger } from "../../overlays/dialog";
import AlertDialog from "./AlertDialog.astro";
import AlertDialogAction from "./AlertDialogAction.astro";
import AlertDialogCancel from "./AlertDialogCancel.astro";
import AlertDialogDescription from "./AlertDialogDescription.astro";
import AlertDialogTitle from "./AlertDialogTitle.astro";
export {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogDescription,
AlertDialogTitle,
AlertDialogTrigger,
};
export default AlertDialog;
What you get
The component source, copied into your project by npx astrocraft-ui add overlays/alert-dialog — no package dependency, no CSS to fight. Whatever the paid blocks compose, this is it.