Delete Confirmation Modal

Introduction: From Browser Alerts to a Designed Delete Modal

In the previous lesson, you built the Task Detail Page, where users could edit a task or delete it.
However, the delete action used a simple browser confirmation dialog (confirm()), which works but feels plain and inconsistent with your app’s design.

In this unit, you’ll replace that with a custom modal — a professionally styled confirmation box that fits your app’s visual identity and provides a better user experience.

You’ll learn how to:

  • Build a reusable Modal component using React and Tailwind CSS.
  • Handle open and close states, including basic Escape-key keyboard handling.
  • Connect the modal to the delete action in the Task Detail Page.

By the end, deleting a task will feel smoother, safer, and better integrated into your UI.

Building the Modal Component

A modal is a dialog box that appears over your page content and demands user attention.
It’s often used for confirmations or important actions like deleting data.
While it’s open, the background content is visually dimmed, keeping attention on the modal.

This is a simplified learning modal: it handles Escape-to-close and clear button actions, but it does not implement a full production dialog accessibility baseline such as focus trapping, automatic initial focus placement, focus restoration after close, or making background content inert.

Here’s the complete code for your Modal component:

'use client';
import { ReactNode, useEffect } from 'react';
import { Button } from './Button';

export function Modal({
  isOpen,
  onClose,
  onConfirm,
  title,
  children,
}: {
  isOpen: boolean;
  onClose: () => void;
  onConfirm: () => void;
  title: string;
  children: ReactNode;
}) {
  useEffect(() => {
    const handler = (e: KeyboardEvent) => e.key === 'Escape' && onClose();
    if (isOpen) document.addEventListener('keydown', handler);
    return () => document.removeEventListener('keydown', handler);
  }, [isOpen, onClose]);

  if (!isOpen) return null;

  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
      <div className="bg-white rounded-lg shadow-lg w-full max-w-md p-6">
        <h2 className="text-lg font-semibold mb-4">{title}</h2>
        <div className="mb-6 text-sm text-gray-700">{children}</div>
        <div className="flex justify-end gap-2">
          <Button variant="secondary" onClick={onClose}>Cancel</Button>
          <Button variant="danger" onClick={onConfirm}>Delete</Button>
        </div>
      </div>
    </div>
  );
}

The Modal component is a self-contained, reusable unit. Let’s break it down.

Props Overview

The component receives the following props:

  • isOpen — determines whether the modal is visible or hidden.
  • onClose — a function to call when the user cancels or presses Escape.
  • onConfirm — a function to call when the user confirms the action (deletion).
  • title — text displayed at the top of the modal.
  • children — any additional content or message passed between the modal’s opening and closing tags.

Visibility Logic

if (!isOpen) return null;

When the modal is closed (isOpen is false), it renders nothing at all — React returns null.
When isOpen is true, the modal markup is rendered in front of everything else on the page.

The Visual Layout

The main container:

<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
  • fixed inset-0 makes the modal cover the entire viewport.
  • bg-black/50 creates a semi-transparent dark background overlay behind the modal.

The inner div with max-w-md defines the centered white modal box.

Inside the modal box:

  • The title (<h2>) shows the dialog title (like “Delete Task”).
  • The children section displays any custom text passed in (“Are you sure you want to delete…”).
  • Two buttons are aligned to the right: Cancel and Delete.

The useEffect Hook: Handling the Escape Key

useEffect(() => {
  const handler = (e: KeyboardEvent) => e.key === 'Escape' && onClose();
  if (isOpen) document.addEventListener('keydown', handler);
  return () => document.removeEventListener('keydown', handler);
}, [isOpen, onClose]);

Let’s unpack this step-by-step:

Purpose:
When the modal is open, users should be able to press the Escape key to close it — just like in native apps.

How it works:

  1. The function handler listens for keydown events.
  2. If the pressed key is 'Escape', it calls onClose() to close the modal.

Conditional Binding:

  • The event listener is added only when the modal is open (if (isOpen)).
  • When the modal closes, the cleanup function removes the listener immediately.

Why it’s inside useEffect:

  • This ensures the listener is only attached to the document when needed.
  • It prevents multiple handlers from stacking or persisting after the modal unmounts.

Dependencies:

  • [isOpen, onClose] means React will re-run this effect if either of these values change, ensuring that the listener is always up to date.

This design keeps the modal lightweight while introducing basic keyboard handling and React’s best practices for managing global event listeners.

Connecting the Modal to the Delete Action

Now that the Modal is ready, let’s connect it to your Task Detail Page so it opens when a user clicks the Delete button.

Here’s the updated page implementation:

'use client';
import { useState } from 'react';
import { useParams, useRouter } from 'next/navigation';
import useSWR, { mutate } from 'swr';
import { api } from '@/lib/apiClient';
import { TaskForm, TaskFormValues } from '@/components/tasks/TaskForm';
import { Button } from '@/components/ui/Button';
import { Modal } from '@/components/ui/Modal';
import { useToast } from '@/components/ui/Toast';

export default function TaskDetailPage() {
  const { id } = useParams<{ id: string }>();
  const router = useRouter();
  const toast = useToast();
  const [showDelete, setShowDelete] = useState(false);
  const key = `/api/tasks/${id}`;
  const { data: task, error, isLoading } = useSWR(key, api.get<any>);

  const handleUpdate = async (values: TaskFormValues) => {
    const res = await api.put(key, { ...values, completed: task?.completed ?? false });
    if ('error' in res) toast.error('Failed to update');
    else {
      toast.success('Task updated');
      mutate('/api/tasks');
      mutate(key);
    }
  };

  const handleDelete = async () => {
    const res = await api.del(key);
    if ('error' in res) toast.error('Failed to delete');
    else {
      toast.success('Task deleted');
      mutate('/api/tasks');
      router.push('/tasks');
    }
  };

  if (isLoading) return <div className="text-gray-500">Loading…</div>;
  if (error || !task || ('error' in task)) return <div className="text-red-600">Task not found</div>;

  return (
    <div className="space-y-6">
      <div className="flex items-center justify-between">
        <h1 className="text-2xl font-semibold">Edit Task</h1>
        <div className="space-x-2">
          <Button variant="danger" onClick={() => setShowDelete(true)}>Delete</Button>
        </div>
      </div>

      <div className="rounded-lg border bg-white p-4">
        <TaskForm initialValues={task} onSubmit={handleUpdate} submitLabel="Save" />
      </div>

      <Modal
        isOpen={showDelete}
        onClose={() => setShowDelete(false)}
        onConfirm={handleDelete}
        title="Delete Task"
      >
        Are you sure you want to delete this task? This action cannot be undone.
      </Modal>
    </div>
  );
}

Breaking Down the Logic

State Management

const [showDelete, setShowDelete] = useState(false);

Controls whether the modal is visible.
When showDelete is true, the modal appears; otherwise, it stays hidden.

Opening the Modal

<Button variant="danger" onClick={() => setShowDelete(true)}>Delete</Button>

Clicking the Delete button sets showDelete(true) — opening the modal.

Modal Configuration

<Modal
  isOpen={showDelete}
  onClose={() => setShowDelete(false)}
  onConfirm={handleDelete}
  title="Delete Task"
>
  Are you sure you want to delete this task? This action cannot be undone.
</Modal>
  • isOpen is tied to the state variable.
  • onClose sets it back to false, closing the modal.
  • onConfirm executes handleDelete, performing the actual deletion.

The Delete Logic

const handleDelete = async () => {
  const res = await api.del(key);
  if ('error' in res) toast.error('Failed to delete');
  else {
    toast.success('Task deleted');
    mutate('/api/tasks');
    router.push('/tasks');
  }
};

Explanation:

  • Sends a DELETE request to the backend via the api.del() helper.

On success:

  • Shows a confirmation toast.
  • Calls mutate('/api/tasks') to refresh the list cache.
  • Redirects the user back to /tasks using router.push().

This flow ensures that:

  • The modal always asks for confirmation.
  • Users can cancel safely or confirm confidently.
  • Data and UI remain in sync after the action.

Summary

In this lesson, you:

  • Replaced the native confirm() dialog with a reusable, accessible Modal component.
  • Learned how to structure the modal with React props and conditional rendering.
  • Used useEffect to listen for the Escape key and handle cleanup properly.
  • Connected the modal to the delete action in the Task Detail Page.
  • Maintained good UX by giving users visual feedback via toasts and redirecting after deletion.

You’ve now built a safer, cleaner, and more modern delete confirmation workflow.
In the next and final unit, you’ll enhance interactivity further by implementing optimistic updates and a “Mark Complete / Incomplete” toggle — so changes reflect instantly in the UI.

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal