Delete Confirmation Dialog with Summary

A delete confirmation dialog shown open over a dimmed settings page. It has a bin icon, a question as the title, one line on the consequence, a three-part summary of what will be removed and two buttons: Keep project and Delete project. The close button, the Keep button, the Escape key and a click on the dimmed area all close it, and the Delete project button on the page behind opens it again. On phones the dialog sits at the bottom of the frame and the buttons stack with the destructive one on top. The whole thing is drawn inside a frame with a set height so it previews in a box.

Code

import { useEffect, useRef, useState } from "react";

const project = "Marlow Coffee rebrand";

const losses = [
  { label: "Pages and drafts", value: "23" },
  { label: "Uploaded files", value: "148" },
  { label: "Form responses", value: "1,062" },
];

const settings = [
  { label: "Project name", value: project },
  { label: "Owner", value: "Nadia Okafor" },
  { label: "Visibility", value: "Private to the team" },
];

export default function DeleteConfirmationDialogWithSummary() {
  const [open, setOpen] = useState(true);
  const [deleted, setDeleted] = useState(false);
  const cancelRef = useRef<HTMLButtonElement>(null);
  const triggerRef = useRef<HTMLButtonElement>(null);
  const firstRender = useRef(true);

  useEffect(() => {
    if (firstRender.current) {
      firstRender.current = false;
      return;
    }
    if (open) cancelRef.current?.focus();
    else triggerRef.current?.focus();
  }, [open]);

  const confirm = () => {
    setDeleted(true);
    setOpen(false);
  };

  return (
    <div className="relative h-[640px] overflow-hidden bg-stone-100 text-stone-900">
      <div aria-hidden={open} className="mx-auto max-w-2xl px-5 py-10 sm:py-14">
        <h2 className="text-xl font-semibold tracking-tight">Project settings</h2>
        <dl className="mt-6 divide-y divide-stone-200 rounded-2xl bg-white px-5 ring-1 ring-stone-200">
          {settings.map((row) => (
            <div key={row.label} className="flex items-center justify-between gap-4 py-4 text-sm">
              <dt className="text-stone-600">{row.label}</dt>
              <dd className="text-right font-medium">{deleted && row.label === "Project name" ? "Deleted" : row.value}</dd>
            </div>
          ))}
        </dl>
        <div className="mt-6 flex flex-col gap-4 rounded-2xl bg-white p-5 ring-1 ring-stone-200 sm:flex-row sm:items-center sm:justify-between">
          <div>
            <h3 className="text-sm font-semibold">{deleted ? "Project deleted" : "Delete this project"}</h3>
            <p className="mt-1 text-sm text-stone-600">{deleted ? "This is a demo, so you can bring it back." : "Removes the project and everything in it for the whole team."}</p>
          </div>
          <button
            ref={triggerRef}
            type="button"
            tabIndex={open ? -1 : 0}
            onClick={() => (deleted ? setDeleted(false) : setOpen(true))}
            className={`min-h-[40px] flex-none rounded-full px-4 text-sm font-semibold transition focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 ${deleted ? "bg-stone-900 text-white hover:bg-stone-700 focus-visible:ring-stone-900" : "text-rose-700 ring-1 ring-inset ring-rose-300 hover:bg-rose-50 focus-visible:ring-rose-600"}`}
          >
            {deleted ? "Restore demo" : "Delete project"}
          </button>
        </div>
      </div>

      {open && (
        <div className="absolute inset-0 flex items-end justify-center bg-stone-950/60 p-3 sm:items-center sm:p-6" onClick={() => setOpen(false)}>
          <div
            role="dialog"
            aria-modal="true"
            aria-labelledby="delete-dialog-title"
            aria-describedby="delete-dialog-text"
            onClick={(event) => event.stopPropagation()}
            onKeyDown={(event) => { if (event.key === "Escape") setOpen(false); }}
            className="relative w-full max-w-md rounded-3xl bg-white p-6 shadow-2xl shadow-stone-950/30 sm:p-7"
          >
            <button type="button" onClick={() => setOpen(false)} aria-label="Close dialog" className="absolute right-3 top-3 flex h-10 w-10 items-center justify-center rounded-full text-stone-600 transition hover:bg-stone-100 hover:text-stone-900 focus:outline-none focus-visible:ring-2 focus-visible:ring-rose-600">
              <svg viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" aria-hidden="true" className="h-4 w-4"><path d="m5 5 10 10M15 5 5 15" /></svg>
            </button>

            <span className="flex h-11 w-11 items-center justify-center rounded-full bg-rose-100 text-rose-700">
              <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="h-5 w-5"><path d="M4.5 7h15M9.5 7V5a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1v2M6.5 7l.8 11a2 2 0 0 0 2 1.9h5.4a2 2 0 0 0 2-1.9l.8-11M10.3 11v5M13.7 11v5" /></svg>
            </span>
            <h2 id="delete-dialog-title" className="mt-4 pr-8 text-lg font-semibold tracking-tight">Delete {project}?</h2>
            <p id="delete-dialog-text" className="mt-2 text-sm leading-relaxed text-stone-600">
              This removes the project for all 4 team members. It cannot be undone after 30 days.
            </p>

            <dl className="mt-5 grid grid-cols-3 divide-x divide-stone-200 rounded-2xl bg-stone-50 py-3.5 text-center ring-1 ring-inset ring-stone-200">
              {losses.map((loss) => (
                <div key={loss.label} className="flex flex-col-reverse justify-end px-2">
                  <dt className="mt-0.5 text-xs leading-tight text-stone-600">{loss.label}</dt>
                  <dd className="text-lg font-semibold tabular-nums tracking-tight">{loss.value}</dd>
                </div>
              ))}
            </dl>

            <div className="mt-6 flex flex-col-reverse gap-2.5 sm:flex-row sm:justify-end">
              <button ref={cancelRef} type="button" onClick={() => setOpen(false)} className="min-h-[44px] rounded-full px-5 text-sm font-semibold text-stone-900 ring-1 ring-inset ring-stone-300 transition hover:bg-stone-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-stone-900 focus-visible:ring-offset-2">
                Keep project
              </button>
              <button type="button" onClick={confirm} className="min-h-[44px] rounded-full bg-rose-600 px-5 text-sm font-semibold text-white transition hover:bg-rose-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-rose-600 focus-visible:ring-offset-2">
                Delete project
              </button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

Install with one command: npx shadcn@latest add https://we.inc/r/delete-confirmation-dialog-with-summary.json

When to use it

How to customize it

Accessibility

Questions

How do I build a modal in React with Tailwind?

Keep an `open` value in state and render an overlay only when it is true. The overlay covers the screen with a dark, part-transparent background and centres the dialog with flex. Clicking the overlay sets `open` to false, and the dialog stops that click from passing through.

Which button should come first in a delete dialog?

Put the safe choice where focus lands and make the destructive one visually distinct, usually red and on the right. Label both with what they do, such as Keep project and Delete project. Plain Yes and No force people to reread the question.

When should I ask for confirmation at all?

Only for actions that are hard to reverse. If something can be undone, do it straight away and offer Undo in a toast. Too many confirmation dialogs train people to click through without reading.

How do I stop the page behind from scrolling?

When the dialog opens, add `overflow-hidden` to the `body` element and remove it on close. Inside this demo frame it is not needed, because the frame has a set height and hides its overflow.

Why is the dialog not fixed to the window in this example?

It is positioned inside a frame so it can be shown in a preview box. Switching the overlay from `absolute` to `fixed` makes it cover the whole window, which is what you want on a live page.

Related: Dismissible Alert Set: Success, Warning, Error and Info, Dark Toast Notification Stack with Undo and Progress, Button Set: Primary, Secondary, Ghost, Icon and Loading.

More modals and dialogs or all free UI components. Start for free to have a full site built around it.

More in Modals

We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.

Product

Who It's For

Features

Resources

Company

View Sitemap