Alert Dialog
A modal that interrupts the user and waits for a deliberate answer.
import { Div } from "@implementjs/core";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/lib/components/ui/alert-dialog";
export default function AlertDialogDemo() {
return AlertDialog(
AlertDialogTrigger({ variant: "outline" }, "Delete account"),
AlertDialogContent(
Div(
{ class: "grid gap-1.5" },
AlertDialogTitle("Are you absolutely sure?"),
AlertDialogDescription(
"This action cannot be undone. This will permanently delete your account and remove your data from our servers.",
),
),
Div(
{ class: "flex justify-end gap-2" },
AlertDialogCancel("Cancel"),
AlertDialogAction({ variant: "destructive" }, "Delete account"),
),
),
);
}Installation
npx jsrepo add @implementjs/ui/alert-dialog
jsrepo pulls button along with it.
Copy the file below to src/lib/components/ui/alert-dialog.ts. It imports cn from utils.ts, which belongs at src/lib/utils.ts, and button from the same directory — copy those in beside it too.
import { type Child, type ComponentProps } from "@implementjs/core";
import {
AlertDialog as AlertDialogPrimitive,
AlertDialogAction as AlertDialogActionPrimitive,
AlertDialogCancel as AlertDialogCancelPrimitive,
AlertDialogContent as AlertDialogContentPrimitive,
AlertDialogDescription as AlertDialogDescriptionPrimitive,
AlertDialogOverlay as AlertDialogOverlayPrimitive,
AlertDialogPortal as AlertDialogPortalPrimitive,
AlertDialogTitle as AlertDialogTitlePrimitive,
AlertDialogTrigger as AlertDialogTriggerPrimitive,
} from "@implementjs/primitives";
import { buttonVariants, type ButtonSize, type ButtonVariant } from "./button";
import { cn } from "@/lib/utils";
import { createComponent } from "@implementjs/primitives";
export type AlertDialogProps = ComponentProps<typeof AlertDialogPrimitive>;
export type AlertDialogTriggerProps = ComponentProps<typeof AlertDialogTriggerPrimitive> & {
variant?: ButtonVariant;
size?: ButtonSize;
};
export type AlertDialogOverlayProps = ComponentProps<typeof AlertDialogOverlayPrimitive>;
export type AlertDialogContentProps = ComponentProps<typeof AlertDialogContentPrimitive> & {
/** Props for the overlay the content renders behind itself. */
overlay?: AlertDialogOverlayProps;
};
export type AlertDialogCancelProps = ComponentProps<typeof AlertDialogCancelPrimitive> & {
variant?: ButtonVariant;
size?: ButtonSize;
};
export type AlertDialogActionProps = ComponentProps<typeof AlertDialogActionPrimitive> & {
variant?: ButtonVariant;
size?: ButtonSize;
};
export type AlertDialogTitleProps = ComponentProps<typeof AlertDialogTitlePrimitive>;
export type AlertDialogDescriptionProps = ComponentProps<typeof AlertDialogDescriptionPrimitive>;
export const AlertDialogPortal = AlertDialogPortalPrimitive;
export const AlertDialog = createComponent(function AlertDialog(
props: AlertDialogProps,
...children: Child[]
) {
return AlertDialogPrimitive(props, ...children);
});
export const AlertDialogTrigger = createComponent(function AlertDialogTrigger(
{
class: className,
variant = "default",
size = "default",
type = "button",
...props
}: AlertDialogTriggerProps,
...children: Child[]
) {
return AlertDialogTriggerPrimitive(
{
type,
...props,
"data-slot": "alert-dialog-trigger",
"data-variant": variant,
"data-size": size,
class: cn(buttonVariants({ variant, size }), className),
},
...children,
);
});
export const AlertDialogOverlay = createComponent(function AlertDialogOverlay(
{ class: className, ...props }: AlertDialogOverlayProps,
...children: Child[]
) {
return AlertDialogOverlayPrimitive(
{
...props,
"data-slot": "alert-dialog-overlay",
class: cn(
"fixed inset-0 z-[calc(50+var(--ip-nested-level,0))] bg-black/50",
"transition-[opacity,display] duration-150 ease-[cubic-bezier(0.16,1,0.3,1)] transition-discrete motion-reduce:transition-none",
"data-[state=open]:block data-[state=open]:opacity-100",
"data-[state=closed]:pointer-events-none data-[state=closed]:hidden data-[state=closed]:opacity-0",
"starting:data-[state=open]:opacity-0",
"data-[nested]:bg-transparent",
className,
),
},
...children,
);
});
export const AlertDialogContent = createComponent(function AlertDialogContent(
{ class: className, overlay = {}, ...props }: AlertDialogContentProps,
...children: Child[]
) {
return AlertDialogPortal(
AlertDialogOverlay(overlay),
AlertDialogContentPrimitive(
{
...props,
"data-slot": "alert-dialog-content",
class: cn(
"fixed top-1/2 left-1/2 z-[calc(50+var(--ip-nested-level,0))] grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-lg border bg-background p-6 text-foreground shadow-lg outline-none sm:max-w-lg",
"transition-[opacity,scale,translate,display] duration-150 ease-[cubic-bezier(0.16,1,0.3,1)] transition-discrete motion-reduce:transition-none",
"data-[state=open]:grid data-[state=open]:scale-[calc(1-0.05*var(--ip-nested-count,0))] data-[state=open]:opacity-100",
"data-[state=closed]:pointer-events-none data-[state=closed]:hidden data-[state=closed]:scale-95 data-[state=closed]:opacity-0",
"starting:data-[state=open]:opacity-0 starting:data-[state=open]:scale-95",
"data-[nested-open]:-translate-y-[calc(50%+(0.5rem*var(--ip-nested-count,0)))]",
className,
),
},
...children,
),
);
});
export const AlertDialogTitle = createComponent(function AlertDialogTitle(
{ class: className, ...props }: AlertDialogTitleProps,
...children: Child[]
) {
return AlertDialogTitlePrimitive(
{
...props,
"data-slot": "alert-dialog-title",
class: cn("text-lg leading-none font-semibold", className),
},
...children,
);
});
export const AlertDialogDescription = createComponent(function AlertDialogDescription(
{ class: className, ...props }: AlertDialogDescriptionProps,
...children: Child[]
) {
return AlertDialogDescriptionPrimitive(
{
...props,
"data-slot": "alert-dialog-description",
class: cn("text-sm text-muted-foreground", className),
},
...children,
);
});
export const AlertDialogCancel = createComponent(function AlertDialogCancel(
{
class: className,
variant = "outline",
size = "default",
type = "button",
...props
}: AlertDialogCancelProps,
...children: Child[]
) {
return AlertDialogCancelPrimitive(
{
type,
...props,
"data-slot": "alert-dialog-cancel",
"data-variant": variant,
"data-size": size,
class: cn(buttonVariants({ variant, size }), className),
},
...children,
);
});
export const AlertDialogAction = createComponent(function AlertDialogAction(
{
class: className,
variant = "default",
size = "default",
type = "button",
...props
}: AlertDialogActionProps,
...children: Child[]
) {
return AlertDialogActionPrimitive(
{
type,
...props,
"data-slot": "alert-dialog-action",
"data-variant": variant,
"data-size": size,
class: cn(buttonVariants({ variant, size }), className),
},
...children,
);
});Usage
An alert dialog is the dialog with the escape hatches removed: no close button in the corner, and no dismissing by clicking the overlay. The only ways out are AlertDialogCancel and AlertDialogAction, which is the point — the user has to answer.
AlertDialogContent renders its own AlertDialogOverlay inside an AlertDialogPortal, so the scrim is never yours to place. All three buttons take variant and size from the button styles, so a destructive confirmation is one prop.
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/lib/components/ui/alert-dialog";
AlertDialog(
AlertDialogTrigger({ variant: "destructive" }, "Delete account"),
AlertDialogContent(
AlertDialogTitle("Are you absolutely sure?"),
AlertDialogDescription("This permanently deletes your account."),
AlertDialogCancel("Cancel"),
AlertDialogAction({ variant: "destructive" }, "Delete account"),
),
);
Stacking
The overlay and the content read --ip-nested-level and --ip-nested-count, which the primitive sets when dialogs open on top of one another. A nested alert dialog's overlay renders transparent instead of darkening the page a second time, and the dialog underneath scales back and shifts up so the stack stays legible.
API Reference
Every prop the styling does not consume is forwarded to the Alert Dialog primitive, so the tables below are the whole surface — the behavior props and the styling ones together.
AlertDialog
The root. Owns whether the alert dialog is open and provides that to the parts inside it.
| Prop | Type | Default | Description |
|---|---|---|---|
open | Signal<boolean> | boolean | false | The open state. Pass a signal to control it from outside; a boolean seeds uncontrolled state. |
preventScroll | boolean | true | When true, the page behind cannot scroll while the alert dialog is open. The overlay and panel can still scroll if you give them overflow. |
AlertDialogTrigger
Toggles the alert dialog open and closed. Clicking a different trigger keeps it open and remembers that button for focus return. Renders a Button; extra props are forwarded onto it.
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" | "default" | Which button style the part renders with. Also set as data-variant. |
size | "default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | "default" | The button's height and padding scale. Also set as data-size. |
default | boolean | false | When the alert dialog starts open, return focus to this trigger instead of the first one in the tree. |
| Data attribute | Value |
|---|---|
[data-alert-dialog-trigger] | Present |
[data-state] | "open" | "closed" |
AlertDialogOverlay
The backdrop behind the panel. Style it against data-state; the primitive does not hide it for you. Styled as a fixed scrim that fades in and out; a nested alert dialog's overlay renders transparent so the stack does not darken twice. AlertDialogContent renders one for you — this export is for composing a panel of your own. Renders a Div; extra props are forwarded onto it.
| Data attribute | Value |
|---|---|
[data-alert-dialog-overlay] | Present |
[data-state] | "open" | "closed" |
[data-nested] | Present when this dialog is nested in another |
[data-nested-open] | Present when a nested dialog is open |
[data-nested-count] | Number of open nested dialogs |
[data-nested-level] | Depth in the stack; 0 is the outermost dialog |
| CSS variable | Description |
|---|---|
--ip-nested-count | How many nested dialogs are open above this one. Use it to scale or translate the parent in a stack, e.g. scale(calc(1 - 0.05 * var(--ip-nested-count))). |
--ip-nested-level | This dialog's depth in the stack, 0 for the outermost. Raise z-index with it so nested dialogs paint above their parent, e.g. z-index: calc(50 + var(--ip-nested-level)). |
AlertDialogContent
The panel. Sets role="alertdialog" and aria-modal. Clicking outside does not dismiss it; Escape still cancels. Style it against data-state; the primitive does not hide or position it for you. Styled as a centered panel that scales in, and shifts up as further dialogs stack on top of it. Renders its own AlertDialogOverlay inside an AlertDialogPortal, so neither has to be placed by hand. Renders a Div; extra props are forwarded onto it.
| Prop | Type | Default | Description |
|---|---|---|---|
overlay | AlertDialogOverlayProps | {} | Props for the overlay the content renders behind itself. |
| Data attribute | Value |
|---|---|
[data-alert-dialog-content] | Present |
[data-state] | "open" | "closed" |
[data-nested] | Present when this dialog is nested in another |
[data-nested-open] | Present when a nested dialog is open |
[data-nested-count] | Number of open nested dialogs |
[data-nested-level] | Depth in the stack; 0 is the outermost dialog |
| CSS variable | Description |
|---|---|
--ip-nested-count | How many nested dialogs are open above this one. Use it to scale or translate the parent in a stack, e.g. scale(calc(1 - 0.05 * var(--ip-nested-count))). |
--ip-nested-level | This dialog's depth in the stack, 0 for the outermost. Raise z-index with it so nested dialogs paint above their parent, e.g. z-index: calc(50 + var(--ip-nested-level)). |
AlertDialogTitle
The heading. Put it inside the content. Wires up aria-labelledby on the panel. Renders a H2; extra props are forwarded onto it.
| Data attribute | Value |
|---|---|
[data-alert-dialog-title] | Present |
AlertDialogDescription
Supporting text. Put it inside the content. Wires up aria-describedby on the panel. Renders a P; extra props are forwarded onto it.
| Data attribute | Value |
|---|---|
[data-alert-dialog-description] | Present |
AlertDialogPortal
Renders its children into another DOM parent so the overlay and panel escape overflow and stacking. This is the core Portal helper; context still resolves from where the portal is declared.
| Prop | Type | Default | Description |
|---|---|---|---|
to | HTMLElement | Readable<HTMLElement> | document.body | The element to mount into. Also available as chained .To(target). |
disabled | boolean | Readable<boolean> | false | Mount in place instead of teleporting. Keep nested dialogs portaled so they stack above the parent. Also available as chained .Disabled(value). |
AlertDialogCancel
Closes without confirming. Receives focus when the alert dialog opens, so Enter or Space backs out instead of confirming. Renders a Button; extra props are forwarded onto it.
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" | "outline" | Which button style the part renders with. Also set as data-variant. |
size | "default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | "default" | The button's height and padding scale. Also set as data-size. |
| Data attribute | Value |
|---|---|
[data-alert-dialog-cancel] | Present |
AlertDialogAction
Confirms and closes when clicked. Attach the actual work to onClick. Renders a Button; extra props are forwarded onto it.
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" | "default" | Which button style the part renders with. Also set as data-variant. |
size | "default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | "default" | The button's height and padding scale. Also set as data-size. |
| Data attribute | Value |
|---|---|
[data-alert-dialog-action] | Present |