Disclosure & OverlaysAlert Dialog
A modal that demands an answer — no X, no outside-click, two labeled exits.
The thinking
A regular dialog is a place you can wander out of; this one is a question you have to answer. It exists for the moment a choice can't be undone, so the whole component is built to make leaving cost something. The one decision everything else rests on: I take the Radix alert-dialog primitive, which already refuses outside-clicks and ships no close button, and I leave it that way — no X added back, no scrim dismiss restored. The footer reuses
buttonVariants from the button rather than inventing its own, so a confirmation is held to the same bar as any button, and the destructive choice rides the same destructive variant it would anywhere else. What lifts it past a default modal is that every casual escape route is a deliberate refusal: the pointer's only ways out are two buttons you have to read and aim at, and the ordering, the color, and the silence on close all push the weight onto that one decision.Interface design
The sheet is the dialog's —
max-w-md, bg-card inside a border-border-faint hairline, p-6 — duplicated into this file on purpose, because the catalog shows each component's source whole. The type does quiet work: the title carries text-balance so a two-line question breaks evenly, and the description gets text-pretty so the consequences read without orphans. In the footer the destructive action sits at the far end of the reading order, after the cancel, resting bright on destructive-bright like every destructive button. Distance and color both ask “are you sure” before the copy gets a chance to. The cancel wears the outline variant: present, legible, deliberately quieter than the thing it's protecting you from.Interaction
Most components here try to infer what you meant; this one turns inference off. A click on the scrim usually reads as “dismiss”, but next to an irreversible choice a stray click is ambiguity, and ambiguity can't answer the question — so the Radix alert-dialog primitive swallows every outside interaction and I don't add the close button back. Escape still cancels: an Esc is no stray click, it's an explicit no, so the keyboard keeps its exit. The surface motion is the dialog's exactly —
pop-in at 200ms from scale: 0.96, pop-out at 150ms, both on cubic-bezier(0.23, 0.88, 0.26, 0.92), scrim on pure opacity, centering parked in a standalone [translate:-50%_-50%] so the keyframes never fight it. The urgency lives in the exits, not in a louder entrance.Sound design
It announces itself on open — the root plays
SOUNDS.open via onOpenChange, with the trigger data-silent so the tap doesn't stack on the announcement. Close is silent on purpose: either button is the answer, and the SoundLayer's tap on cancel or confirm is the only voice the exit needs. A close chord here would score the decision twice.Empathy
On open, focus doesn't go where a dialog would normally put it — the Radix primitive redirects its autofocus to the cancel button, so a reflexive Enter backs out instead of destroying something. Autopilot is made safe. The DOM keeps the cancel before the action, so tab order reaches the safe exit first even when
flex-col-reverse stacks the action on top on small screens, and both buttons inherit the button's focus-visible:ring-1 so the keyboard can see where it stands. Screen readers get role="alertdialog" with the title and description wired in by Radix, so the question arrives in full. motion-reduce:animate-none sits on both the scrim and the sheet — under reduced motion it simply appears. And there's no internal scroll, on purpose: this holds one question and two answers, and anything long enough to scroll belongs in a dialog instead."use client";
import * as React from "react";
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
import { type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { useSound } from "@/hooks/useSound";
import { SOUNDS } from "@/lib/sounds";
import { buttonVariants } from "../button/button";
// it announces itself on open; close is silent. either footer button IS
// the answer, and the layer's tap on it is the only voice the exit needs.
const AlertDialog = ({
onOpenChange,
...props
}: React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Root>) => {
const [playOpen] = useSound(SOUNDS.open);
return (
<AlertDialogPrimitive.Root
onOpenChange={(open) => {
if (open) playOpen();
onOpenChange?.(open);
}}
{...props}
/>
);
};
AlertDialog.displayName = "AlertDialog";
const AlertDialogTrigger = ({
ref,
...props
}: React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Trigger> & {
ref?: React.Ref<React.ComponentRef<typeof AlertDialogPrimitive.Trigger>>;
}) => <AlertDialogPrimitive.Trigger ref={ref} data-silent {...props} />;
AlertDialogTrigger.displayName = AlertDialogPrimitive.Trigger.displayName;
const AlertDialogPortal = AlertDialogPrimitive.Portal;
const AlertDialogOverlay = ({
ref,
className,
...props
}: React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay> & {
ref?: React.Ref<React.ComponentRef<typeof AlertDialogPrimitive.Overlay>>;
}) => (
<AlertDialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-scrim data-[state=open]:animate-overlay-in data-[state=closed]:animate-overlay-out motion-reduce:animate-none",
className
)}
{...props}
/>
);
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
// same sheet as the dialog — duplicated knowingly, not imported: the
// catalog shows each component's source whole, so shells stay local, minus every casual exit: the primitive
// refuses outside-click and there is no X. standalone `translate`
// keeps centering out of the pop keyframes' transform.
const AlertDialogContent = ({
ref,
className,
...props
}: React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content> & {
ref?: React.Ref<React.ComponentRef<typeof AlertDialogPrimitive.Content>>;
}) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-1/2 top-1/2 z-50 grid w-full max-w-md gap-4 rounded-lg border border-border-faint bg-card p-6 text-card-foreground outline-none [translate:-50%_-50%] data-[state=open]:animate-pop-in data-[state=closed]:animate-pop-out motion-reduce:animate-none",
className
)}
{...props}
/>
</AlertDialogPortal>
);
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-1", className)} {...props} />
);
AlertDialogHeader.displayName = "AlertDialogHeader";
// reading order ends at the action — the destructive choice sits at
// the far end, behind the cancel
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
);
AlertDialogFooter.displayName = "AlertDialogFooter";
const AlertDialogTitle = ({
ref,
className,
...props
}: React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title> & {
ref?: React.Ref<React.ComponentRef<typeof AlertDialogPrimitive.Title>>;
}) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn(
"font-head text-base font-medium text-balance",
className
)}
{...props}
/>
);
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
const AlertDialogDescription = ({
ref,
className,
...props
}: React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description> & {
ref?: React.Ref<
React.ComponentRef<typeof AlertDialogPrimitive.Description>
>;
}) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground text-pretty", className)}
{...props}
/>
);
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName;
// the action wears button styles by variant — destructive when the
// question is destructive, so color asks "are you sure" before the copy
const AlertDialogAction = ({
ref,
className,
variant = "default",
...props
}: React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action> & {
ref?: React.Ref<React.ComponentRef<typeof AlertDialogPrimitive.Action>>;
variant?: VariantProps<typeof buttonVariants>["variant"];
}) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants({ variant }), className)}
{...props}
/>
);
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
const AlertDialogCancel = ({
ref,
className,
...props
}: React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel> & {
ref?: React.Ref<React.ComponentRef<typeof AlertDialogPrimitive.Cancel>>;
}) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(buttonVariants({ variant: "outline" }), className)}
{...props}
/>
);
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
export {
AlertDialog,
AlertDialogTrigger,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
};