# Confirm Dialog
*URL: https://www.grettech.com/components/confirm-dialog*

A confirmation dialog for important or destructive actions.

- Default and destructive variants
- Waits for an async onConfirm, with a loading state
- Focus trapping and keyboard support from Radix Dialog
- Use any element as the trigger

## Install with the shadcn CLI
```bash
npx shadcn@latest add https://www.grettech.com/api/registry/confirm-dialog
```
With its demo: `npx shadcn@latest add https://www.grettech.com/api/registry/confirm-dialog-demo`

**Dependencies:** npm: clsx, tailwind-merge, lucide-react; shadcn/ui: button, dialog

## Manual installation
**Step 1: Install dependencies**
Install the packages this component uses and the shadcn/ui components it builds on. It also needs the `cn` helper in `lib/utils.ts`, which `npx shadcn@latest init` adds.

```bash
npm install clsx tailwind-merge lucide-react
npx shadcn@latest add button dialog
```

**Step 2: Copy the source code**
Add these files to your project components directory.

File: `components/ui/confirm-dialog.tsx`
```tsx
"use client"

import { Button } from "@/components/ui/button"
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from "@/components/ui/dialog"
import { Loader2 } from "lucide-react"
import * as React from "react"
import { useState } from "react"
import { cn } from "@/lib/utils"

type TriggerProps = {
  onClick?: React.MouseEventHandler<HTMLElement>
  "aria-haspopup"?: React.AriaAttributes["aria-haspopup"]
  "aria-expanded"?: boolean
}

/**
 * Opens the dialog from whatever trigger element is passed in. Done by hand
 * instead of `<DialogTrigger asChild>` so it works with both the Radix and
 * the Base UI versions of shadcn/ui's Dialog.
 */
function renderTrigger(
  trigger: React.ReactElement<TriggerProps>,
  open: boolean,
  onOpen: () => void
) {
  return React.cloneElement(trigger, {
    onClick: (e: React.MouseEvent<HTMLElement>) => {
      trigger.props.onClick?.(e)
      if (!e.defaultPrevented) onOpen()
    },
    "aria-haspopup": "dialog",
    "aria-expanded": open,
  })
}

export interface ConfirmDialogProps extends React.HTMLAttributes<HTMLDivElement> {
  title?: string
  description?: string
  triggerText?: string
  confirmText?: string
  cancelText?: string
  variant?: "default" | "destructive"
  /** May return a promise; the dialog stays open until it settles */
  onConfirm?: () => void | Promise<void>
  children?: React.ReactNode
}

export const ConfirmDialog = React.forwardRef<
  HTMLDivElement,
  ConfirmDialogProps
>(
  (
    {
      title = "Are you absolutely sure?",
      description = "This action cannot be undone. This will permanently delete your account and remove your data from our servers.",
      triggerText = "Open Dialog",
      confirmText = "Continue",
      cancelText = "Cancel",
      variant = "default",
      onConfirm,
      children,
      className,
      ...props
    },
    ref
  ) => {
    const [open, setOpen] = useState(false)
    const [pending, setPending] = useState(false)

    const handleConfirm = async () => {
      setPending(true)
      try {
        await onConfirm?.()
        setOpen(false)
      } finally {
        setPending(false)
      }
    }

    return (
      <Dialog open={open} onOpenChange={(next) => !pending && setOpen(next)}>
        {renderTrigger(
          React.isValidElement<TriggerProps>(children) ? (
            children
          ) : (
            <Button variant={variant}>{children || triggerText}</Button>
          ),
          open,
          () => setOpen(true)
        )}
        <DialogContent
          ref={ref}
          className={cn("sm:max-w-[425px]", className)}
          {...props}
        >
          <DialogHeader>
            <DialogTitle>{title}</DialogTitle>
            <DialogDescription>{description}</DialogDescription>
          </DialogHeader>
          <DialogFooter className="mt-4">
            <Button
              variant="outline"
              onClick={() => setOpen(false)}
              disabled={pending}
            >
              {cancelText}
            </Button>
            <Button
              variant={variant}
              onClick={handleConfirm}
              disabled={pending}
              // Destructive actions shouldn't be one Enter press away
              autoFocus={variant !== "destructive"}
            >
              {pending && <Loader2 className="animate-spin" />}
              {confirmText}
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>
    )
  }
)
ConfirmDialog.displayName = "ConfirmDialog"

```

**Step 3: Framework Adjustments (Optional)**
If you are manually copying this code into a Vite or Remix project (without using the CLI), make sure to remove the `"use client";` directive from the very top of the file.

## Props
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
| `title` | `string` | `"Are you absolutely sure?"` | No | Dialog title. |
| `description` | `string` | `"This action cannot be undone. This will permanently delete your account and remove your data from our servers."` | No | Text under the title. |
| `triggerText` | `string` | `"Open Dialog"` | No | Label of the default trigger button, used when children is not set. |
| `confirmText` | `string` | `"Continue"` | No | Label of the confirm button. |
| `cancelText` | `string` | `"Cancel"` | No | Label of the cancel button. |
| `variant` | `"default" \| "destructive"` | `"default"` | No | Style of the confirm button (and the default trigger). |
| `onConfirm` | `() => void \| Promise<void>` | - | No | Called when the user confirms. If it returns a promise, the dialog shows a loading state and stays open until it settles. |
| `children` | `React.ReactNode` | - | No | Trigger element. It receives onClick, so pass a button or a component that forwards onClick. Plain text is wrapped in a Button; without children, a Button with triggerText is used. |
| `className` | `string` | - | No | Extra classes for the dialog content. |
| `...props` | `React.HTMLAttributes<HTMLDivElement>` | - | No | Any other attributes (id, style, aria-*, etc.), passed to the dialog content. |

## Usage
```tsx
"use client"

import { Button } from "@/components/ui/button"
import { ConfirmDialog } from "@/components/ui/confirm-dialog"

export default function ConfirmDialogDemo() {
  return (
    <ConfirmDialog
      title="Delete Account"
      description="Are you sure? This action cannot be reversed."
      confirmText="Yes, delete it"
      variant="destructive"
      cancelText="Cancel"
      onConfirm={() => console.log("Deleted")}
    >
      <Button variant="destructive">Delete Account</Button>
    </ConfirmDialog>
  )
}

```
