Skip to main content
astrocraft-ui/ components · 101

Alert Dialog

Free · MIT

Headless, 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

Install command
npx astrocraft-ui add overlays/alert-dialog

Plain-CSS theme — no build step

Install command
npx astrocraft-ui add overlays/alert-dialog --theme css

Tailwind theme — needs Tailwind v4

Install command
npx astrocraft-ui add overlays/alert-dialog --theme tailwind

Plain-CSS theme, re-pointed onto Lumos tokens

Install command
npx astrocraft-ui add overlays/alert-dialog --theme css --bridge lumos

Tailwind theme, re-pointed onto Lumos tokens

Install command
npx astrocraft-ui add overlays/alert-dialog --theme tailwind --bridge lumos

Live 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.

Delete “Orbit” and its 34 files?

This removes the project for everyone on the team. It cannot be undone.

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.

ComponentSlotsVariantsRuntime stateNative 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
---
// 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
---
// 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
---
// 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
---
// 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
---
// 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>
src/components/ui/overlays/alert-dialog/index.ts
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.