FewTone UI
ComponentsDocs
  • Home

Getting started

  • Introduction
  • Installation
  • MCP Server
  • Smooth UI Skill
  • Roadmap

Components46

  • Accordion
  • Alert Dialog
  • Avatar
  • Badge
  • Breadcrumb
  • Button
  • Calendar
  • Card
  • Checkbox
  • Combobox
  • Command
  • Context Menu
  • Copy Button
  • Date Picker
  • Dialog
  • Dropdown Menu
  • Hover Card
  • Input
  • Input OTP
  • Loading Button
  • Marquee
  • Navigation Menu
  • Number Input
  • Number Ticker
  • Pagination
  • Popover
  • Progress
  • Radio Group
  • Scroll Area
  • Select
  • Sheet
  • Sidebar
  • Skeleton
  • Slider
  • Spinner
  • Stepper
  • Switch
  • Switcher
  • Table
  • Tabs
  • Text Effects
  • Textarea
  • Theme Toggle
  • Toast
  • Toggle Group
  • Tooltip
© 2026 FewTone · MIT licenceGitHub
  1. Components
  2. Alert Dialog

Alert Dialog

pnpm dlx shadcn@latest add …/r/alert-dialog.json

An accessible React alert dialog for Next.js that asks people to confirm destructive actions, with focus starting on Cancel, built on Radix Alert Dialog.

import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"

export function DeleteAccount() {
  return (
    <AlertDialog>
      <AlertDialogTrigger asChild>
        <Button variant="outline">Delete account</Button>
      </AlertDialogTrigger>
      <AlertDialogContent>
        <AlertDialogHeader>
          <AlertDialogTitle>Delete your account?</AlertDialogTitle>
          <AlertDialogDescription>
            Your projects and settings go with it. You can't undo this.
          </AlertDialogDescription>
        </AlertDialogHeader>
        <AlertDialogFooter>
          <AlertDialogCancel>Cancel</AlertDialogCancel>
          <AlertDialogAction variant="destructive" onClick={deleteAccount}>
            Delete account
          </AlertDialogAction>
        </AlertDialogFooter>
      </AlertDialogContent>
    </AlertDialog>
  )
}
delete-project.tsx
import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";

export default function AlertDialogDeleteProject() {
  return (
    <AlertDialog>
      <AlertDialogTrigger asChild>
        <Button variant="outline">Delete project</Button>
      </AlertDialogTrigger>
      <AlertDialogContent>
        <AlertDialogHeader>
          <AlertDialogTitle>Delete project?</AlertDialogTitle>
          <AlertDialogDescription>
            This permanently deletes Acme Web, with its deployments and domains. You can't undo this.
          </AlertDialogDescription>
        </AlertDialogHeader>
        <AlertDialogFooter>
          <AlertDialogCancel>Cancel</AlertDialogCancel>
          <AlertDialogAction variant="destructive">Delete project</AlertDialogAction>
        </AlertDialogFooter>
      </AlertDialogContent>
    </AlertDialog>
  );
}

Installation

  1. 1

    Install the dependencies.

    Package manager
    $ pnpm add class-variance-authority motion radix-ui
  2. 2

    Add the FewTone UI tokens to your global CSS file. Skip any variable your project already defines.

    app/globals.css
    :root {
      --size-control-sm: 2rem;
      --size-control-md: 2.25rem;
      --size-control-lg: 2.5rem;
      --size-icon-sm: 0.875rem;
      --size-icon-md: 1rem;
      --size-icon-lg: 1.25rem;
      --size-roll-fade: 0.25em;
      --size-roll-fade-x: 0.5em;
      --duration-instant: 80ms;
      --duration-fast: 150ms;
      --duration-base: 220ms;
      --duration-slow: 350ms;
      --duration-slower: 500ms;
      --z-overlay: 1200;
      --z-modal: 1300;
      --z-popover: 1400;
      --z-toast: 1500;
      --z-tooltip: 1600;
      --focus-ring-width: 2px;
      --focus-ring-offset: 2px;
      --opacity-disabled: 0.5;
      --ease-spring: linear(0, 0.157, 0.48, 0.77, 0.97, 1.07, 1.1, 1.09, 1.05, 1.02, 1, 0.995, 1);
      --destructive-foreground: oklch(1 0 0);
      --overlay: color-mix(in oklch, oklch(0 0 0) 40%, transparent);
    }
    
    .dark {
      --destructive-foreground: oklch(1 0 0);
      --overlay: color-mix(in oklch, oklch(0 0 0) 60%, transparent);
    }
    
    @theme inline {
      --animate-popover-in: popover-in var(--duration-fast) cubic-bezier(0.22, 1, 0.36, 1) both;
      --animate-popover-out: popover-out var(--duration-instant) cubic-bezier(0.55, 0, 1, 0.45) both;
      --animate-collapsible-open: collapsible-open var(--duration-slow) cubic-bezier(0.22, 1, 0.36, 1);
      --animate-collapsible-close: collapsible-close var(--duration-base) cubic-bezier(0.55, 0, 1, 0.45) both;
      --color-destructive-foreground: var(--destructive-foreground);
      --color-overlay: var(--overlay);
      @keyframes popover-in {
        from {
          opacity: 0;
          filter: blur(6px);
          transform: scale(0.96);
        }
        to {
          opacity: 1;
          filter: blur(0);
          transform: scale(1);
        }
      }
      @keyframes popover-out {
        from {
          opacity: 1;
          filter: blur(0);
          transform: scale(1);
        }
        to {
          opacity: 0;
          filter: blur(6px);
          transform: scale(0.96);
        }
      }
      @keyframes collapsible-open {
        from {
          height: 0;
          opacity: 0;
          filter: blur(6px);
        }
        to {
          height: var(--collapsible-height);
          opacity: 1;
          filter: blur(0);
        }
      }
      @keyframes collapsible-close {
        from {
          height: var(--collapsible-height);
          opacity: 1;
          filter: blur(0);
        }
        to {
          height: 0;
          opacity: 0;
          filter: blur(6px);
        }
      }
    }
    
    @media (prefers-reduced-motion: reduce) {
      :root {
        --duration-instant: 1ms;
        --duration-fast: 1ms;
        --duration-base: 1ms;
        --duration-slow: 1ms;
        --duration-slower: 1ms;
      }
    }
  3. 3

    Add Button, Dialog first.

  4. 4

    Copy the source into your project.

    components/ui/alert-dialog.tsx
    "use client";
    
    import { buttonVariants } from "@/components/ui/button";
    import {
      type DialogState,
      dialogOverlayMotion,
      dialogStyles,
      keepMovedFocus,
      useDialogState,
      useDialogWindow,
      useMergedRef,
    } from "@/components/ui/dialog";
    import { cn } from "@/lib/utils";
    import type { VariantProps } from "class-variance-authority";
    import { AnimatePresence, motion } from "motion/react";
    import { AlertDialog as AlertDialogPrimitive } from "radix-ui";
    import { type ComponentProps, createContext, useContext } from "react";
    
    /*
     * Alert Dialog, built on Radix Alert Dialog: a modal that asks people to confirm something, usually an action
     * they can't undo. It's announced as an alert dialog, focus starts on Cancel (the safe choice), Escape
     * cancels, and clicking the backdrop does nothing, so it's never dismissed by accident. Focus goes back to
     * the trigger.
     * - The same window, motion and open state as Dialog, shared from its file: it grows out of its trigger
     *   through the blur on spring.snappy, leaves on a timed duration.base + ease.in, and the backdrop only
     *   fades. Controlled (`open`) or not (`defaultOpen`). While it leaves, clicks go through to the page.
     * - Narrower than Dialog (max-w-md): it holds a question and two answers.
     * - Action and Cancel are buttons (buttonVariants). Use `variant="destructive"` on Action for deleting.
     */
    
    const AlertDialogContext = createContext<DialogState | null>(null);
    
    function useAlertDialog(part: string) {
      const dialog = useContext(AlertDialogContext);
      if (!dialog) throw new Error(`${part} must be used inside AlertDialog`);
      return dialog;
    }
    
    function AlertDialog({
      open,
      defaultOpen,
      onOpenChange,
      ...props
    }: ComponentProps<typeof AlertDialogPrimitive.Root>) {
      const { state, setOpen } = useDialogState({ open, defaultOpen, onOpenChange });
      return (
        <AlertDialogContext.Provider value={state}>
          <AlertDialogPrimitive.Root
            data-slot="alert-dialog"
            open={state.open}
            onOpenChange={setOpen}
            {...props}
          />
        </AlertDialogContext.Provider>
      );
    }
    
    function AlertDialogTrigger({ ref, ...props }: ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
      const dialog = useAlertDialog("AlertDialogTrigger");
      const setRef = useMergedRef(dialog.trigger, ref);
      return <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" ref={setRef} {...props} />;
    }
    
    /** Stays mounted while the dialog is open and while its exit plays */
    function AlertDialogPortal({
      children,
      ...props
    }: Omit<ComponentProps<typeof AlertDialogPrimitive.Portal>, "forceMount">) {
      const { open } = useAlertDialog("AlertDialogPortal");
      return (
        <AnimatePresence initial={false}>
          {open ? (
            <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} forceMount>
              {children}
            </AlertDialogPrimitive.Portal>
          ) : null}
        </AnimatePresence>
      );
    }
    
    function AlertDialogOverlay({
      className,
      style,
      ...props
    }: ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
      const { open } = useAlertDialog("AlertDialogOverlay");
      return (
        <AlertDialogPrimitive.Overlay
          data-slot="alert-dialog-overlay"
          className={cn(dialogStyles.overlay, className)}
          // While it fades out, clicks go through to the page
          style={{ pointerEvents: open ? "auto" : "none", ...style }}
          {...props}
          asChild
          forceMount
        >
          <motion.div {...dialogOverlayMotion} />
        </AlertDialogPrimitive.Overlay>
      );
    }
    
    function AlertDialogContent({
      className,
      children,
      onCloseAutoFocus,
      ...props
    }: ComponentProps<typeof AlertDialogPrimitive.Content>) {
      const dialog = useAlertDialog("AlertDialogContent");
      const windowMotion = useDialogWindow(dialog.trigger);
      return (
        <AlertDialogPortal>
          <AlertDialogOverlay />
          <div data-slot="alert-dialog-positioner" className={dialogStyles.positioner}>
            <AlertDialogPrimitive.Content
              data-slot="alert-dialog-content"
              className={cn(dialogStyles.window, "max-w-md", className)}
              onCloseAutoFocus={(event) => {
                onCloseAutoFocus?.(event);
                keepMovedFocus(event);
              }}
              {...props}
              asChild
              forceMount
            >
              <motion.div {...windowMotion}>{children}</motion.div>
            </AlertDialogPrimitive.Content>
          </div>
        </AlertDialogPortal>
      );
    }
    
    function AlertDialogHeader({ className, ...props }: ComponentProps<"div">) {
      return <div data-slot="alert-dialog-header" className={cn(dialogStyles.header, className)} {...props} />;
    }
    
    /** Cancel, then the action: stacked on phones with the action on top, in a row at the end from sm */
    function AlertDialogFooter({ className, ...props }: ComponentProps<"div">) {
      return <div data-slot="alert-dialog-footer" className={cn(dialogStyles.footer, className)} {...props} />;
    }
    
    function AlertDialogTitle({ className, ...props }: ComponentProps<typeof AlertDialogPrimitive.Title>) {
      return (
        <AlertDialogPrimitive.Title
          data-slot="alert-dialog-title"
          className={cn(dialogStyles.title, className)}
          {...props}
        />
      );
    }
    
    function AlertDialogDescription({
      className,
      ...props
    }: ComponentProps<typeof AlertDialogPrimitive.Description>) {
      return (
        <AlertDialogPrimitive.Description
          data-slot="alert-dialog-description"
          className={cn(dialogStyles.description, className)}
          {...props}
        />
      );
    }
    
    type ButtonStyle = VariantProps<typeof buttonVariants>;
    
    /** Confirms and closes. A Button in every way: `variant="destructive"` for deleting */
    function AlertDialogAction({
      className,
      variant,
      size,
      ...props
    }: ComponentProps<typeof AlertDialogPrimitive.Action> & ButtonStyle) {
      return (
        <AlertDialogPrimitive.Action
          data-slot="alert-dialog-action"
          className={cn(buttonVariants({ variant, size }), className)}
          {...props}
        />
      );
    }
    
    /** Closes without doing anything; it has focus when the dialog opens */
    function AlertDialogCancel({
      className,
      variant = "outline",
      size,
      ...props
    }: ComponentProps<typeof AlertDialogPrimitive.Cancel> & ButtonStyle) {
      return (
        <AlertDialogPrimitive.Cancel
          data-slot="alert-dialog-cancel"
          className={cn(buttonVariants({ variant, size }), className)}
          {...props}
        />
      );
    }
    
    export {
      AlertDialog,
      AlertDialogAction,
      AlertDialogCancel,
      AlertDialogContent,
      AlertDialogDescription,
      AlertDialogFooter,
      AlertDialogHeader,
      AlertDialogOverlay,
      AlertDialogPortal,
      AlertDialogTitle,
      AlertDialogTrigger,
    };
  5. 5

    Update the import paths to match your project.

API reference

AlertDialog

The root. It keeps the open state, so the window can play its exit before it unmounts.

PropTypeDefaultDescription
open?boolean–Whether it's open, when you control it.
defaultOpen?booleanfalseWhether it starts open.
onOpenChange?(open: boolean) => void–Called when it opens or closes: the trigger, Cancel, the action or Escape.

AlertDialogTrigger

The button that opens it. Focus comes back here when it closes.

PropTypeDefaultDescription
asChild?booleanfalseUse your own element, such as a Button, as the trigger.

AlertDialogContent

The window, with its backdrop, in a portal: the same look and motion as Dialog, a little narrower, without a close button. Announced as an alertdialog, named by its title and described by its description. Also accepts every Radix Alert Dialog Content prop.

PropTypeDefaultDescription
onOpenAutoFocus?(event: Event) => void–Runs as focus moves to Cancel. Call event.preventDefault() to focus something else yourself.
onCloseAutoFocus?(event: Event) => void–Runs as focus goes back to the trigger. Call event.preventDefault() to send it elsewhere.
onEscapeKeyDown?(event: KeyboardEvent) => void–Runs when Escape is pressed. Call event.preventDefault() to keep it open.

AlertDialogHeader

Holds the title and description.

PropTypeDefaultDescription

AlertDialogFooter

Holds Cancel and the action: stacked on phones with the action on top, in a row at the end from the sm breakpoint.

PropTypeDefaultDescription

AlertDialogTitle

The question, and the dialog's accessible name. Say what will happen: “Delete project?”.

PropTypeDefaultDescription

AlertDialogDescription

What the action does and whether it can be undone, read out after the name.

PropTypeDefaultDescription

AlertDialogAction

Confirms and closes. It's a Button, so it takes Button's variant and size. Your onClick runs first; call event.preventDefault() there to keep the dialog open.

PropTypeDefaultDescription
variant?"default" | "secondary" | "outline" | "ghost" | "destructive" | "link""default"The button style. Use destructive for actions that remove things.
size?"sm" | "md" | "lg""md"The button height.

AlertDialogCancel

Closes without doing anything. It has focus when the dialog opens.

PropTypeDefaultDescription
variant?"default" | "secondary" | "outline" | "ghost" | "destructive" | "link""outline"The button style.
size?"sm" | "md" | "lg""md"The button height.

AlertDialogPortal

Renders its children into document.body while the dialog is open, and while it leaves. AlertDialogContent already uses it.

PropTypeDefaultDescription
container?HTMLElementdocument.bodyWhere to render instead.

AlertDialogOverlay

The backdrop that dims the page. It only fades, and clicking it does nothing. AlertDialogContent already includes it.

PropTypeDefaultDescription

Related components

  • Dialog

    An accessible React dialog for Next.js and Tailwind CSS: a modal window that grows out of its trigger, keeps focus inside and returns it, built on Radix Dialog.

  • Button

    An accessible React button for Next.js and Tailwind CSS with six variants, three sizes, icons and a soft tint on hover and press, built on a native button.

  • Right-click here
    Context Menu

    An accessible React context menu for Next.js: a right-click menu with a gliding highlight, shortcuts, checkbox and radio items and sub-menus, built on Radix.