webdevpuneet.com

Bulk Delete Confirm Dialog with Undo Countdown — Two-Stage Destructive Action Pattern

Bulk Action Confirm Dialog — Undo Countdown · Modals · Plain HTML, CSS & JS · Live preview

CategoryModals

Copy & paste

Source Code

<div class="demo">
  <button class="trigger" id="openBtn">Delete 7 selected items</button>

  <div class="overlay" id="overlay">
    <div class="dialog" role="alertdialog" aria-modal="true" aria-labelledby="dTitle" aria-describedby="dDesc">
      <div class="dialog-icon">
        <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m3 0-1 14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2L4 6"/></svg>
      </div>
      <h2 id="dTitle">Delete 7 items?</h2>
      <p id="dDesc">This will permanently remove the selected items from your workspace. You can undo this within the next few seconds.</p>

      <div class="dialog-actions">
        <button class="btn ghost" id="cancelBtn">Cancel</button>
        <button class="btn danger" id="confirmBtn">Delete items</button>
      </div>
    </div>

    <div class="undo-toast" id="undoToast" role="status" aria-live="polite">
      <div class="undo-text">
        <strong>7 items deleted</strong>
        <span>Undo available for <span id="undoSecs">5</span>s</span>
      </div>
      <button class="undo-btn" id="undoBtn">Undo</button>
      <div class="undo-bar"><div class="undo-bar-fill" id="undoFill"></div></div>
    </div>
  </div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Two-stage safety pattern: an explicit confirm dialog followed by a countdown-timed undo window, not just one or the other
CSS keyframe animation drives the smooth shrinking countdown bar, decoupled from the JS interval tick rate
Reflow-forcing restart trick lets the same undo animation replay correctly on repeated bulk actions
role="alertdialog" and aria-describedby on the confirm step for correct screen reader announcement
role="status" aria-live="polite" on the undo toast so its appearance is announced without stealing focus
Escape key and click-outside-to-close on the initial confirm dialog
Single shared clearInterval cleanup on both the Undo click and countdown completion, preventing stale timers
Auto-focuses the destructive confirm button on open for fast keyboard confirmation

About this UI Snippet

Bulk Action Confirm Dialog with an Undo Countdown — A Two-Stage Safety Net

Screenshot of the Bulk Action Confirm Dialog — Undo Countdown snippet rendered live

Destructive bulk actions usually get one safety net: a confirm dialog asking "are you sure?" This snippet adds a second one on top of it — after the user confirms, the dialog doesn't immediately finalize the action. Instead it hands off to an undo toast with a visible countdown, so a user who clicks "Delete" reflexively (or realizes a mistake a second later) still has a real window to reverse it before anything is truly gone.

Why a confirm dialog alone isn't always enough

A modal confirm step protects against truly accidental clicks, but it does nothing for the very common case of a user confidently clicking "Delete" and only realizing their mistake — wrong selection, wrong button — a moment *after* confirming. An undo window catches exactly that second category of error, which a yes/no dialog structurally cannot.

The countdown bar is the same duration as the JS timer, driven by CSS

The shrinking bar underneath the undo toast uses a CSS @keyframes shrink animation with animation: shrink 5s linear forwards, set to the identical five-second duration as the setInterval countdown running in JavaScript. Using a CSS animation for the visual bar (rather than updating its width from JS on every tick) keeps the fill perfectly smooth at 60fps regardless of how often the interval callback fires, while the JS timer independently handles the actual "seconds left" number and the moment the action finalizes.

Restarting an already-completed CSS animation

Because the undo window can be triggered more than once in a session, the code removes the .count class, forces a reflow with void undoFill.offsetWidth, and re-adds .count — the reflow-forcing line is a well-known trick to make the browser "forget" that the animation already finished, so restarting the same class re-triggers it from the beginning instead of doing nothing (which is what re-adding an already-present class would otherwise do).

Three clean exits, one shared cleanup path

The undo window can end three ways: the countdown reaches zero and finalizeDelete() runs, the user clicks "Undo" and performUndo() runs, or — in a production version — the user navigates away entirely. Both explicit paths call clearInterval(timer) before doing anything else, which matters because without it, a stale interval from a previous bulk-delete could keep ticking in the background and fire its callback against a toast that's no longer showing.

Where a modal-based undo beats a toast-only pattern

A plain undo *snackbar* (with no preceding confirm step) is fine for low-stakes, easily-reversible actions. For a genuinely destructive bulk operation, pairing an explicit confirm dialog with a visible, countdown-timed undo window gives a user two distinct moments to reconsider — the deliberate "are you sure" click, and the brief grace period immediately after — which is a meaningfully stronger safety pattern for anything a user would be upset to lose permanently.

Step by step

How to Use

  1. 1
    Update the trigger button and item countChange "Delete 7 selected items" and the matching numbers inside the dialog to reflect your real selection count.
  2. 2
    Adjust the undo window lengthChange both the 5s in the CSS @keyframes shrink animation duration and the secondsLeft = 5 starting value in JS together — they must stay in sync.
  3. 3
    Wire finalizeDelete() to your real delete callThis is where the actual API request or state mutation should fire once the countdown reaches zero with no undo.
  4. 4
    Wire performUndo() to restore stateIf your app optimistically removed the items from view already, this is where you'd restore them instead of just closing the toast.
  5. 5
    Test Escape and click-outside behaviorConfirm both close the confirm dialog appropriately before an action has been confirmed.

Real-world uses

Common Use Cases

ADMIN
Bulk Record Deletion
Deleting multiple rows, users, or files from an admin table with a real chance to reverse the action.
Inbox / Message Bulk Actions
Archiving or deleting many emails or messages at once, mirroring the "Undo" pattern popularized by Gmail.
Content Management Bulk Ops
Bulk-unpublishing or removing many CMS entries where an instant, no-questions undo meaningfully reduces support tickets.
FILES
File Manager Multi-Select Delete
Deleting several selected files or folders in a cloud storage or file-manager style interface.
Related: App Store Rating Prompt Modal
See the App Store Rating Prompt Modal for a related modals pattern worth pairing with this one.
Related: Type-to-Confirm Delete Modal
See the Type-to-Confirm Delete Modal for a related modals pattern worth pairing with this one.
Related: What's New Changelog Modal
See the What's New Changelog Modal for a related modals pattern worth pairing with this one.
Related: Report Content Modal with Reason Picker
See the Report Content Modal with Reason Picker for a related modals pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

For genuinely destructive, hard-to-reverse actions, pairing both steps gives two separate moments for a user to catch a mistake — the deliberate confirm click, and the grace period right after. A toast-only pattern is fine for lower-stakes actions, but a confirm step first is a stronger safety net for anything a user would be upset to lose.

There's a small race condition inherent to any timer-based UI; in this implementation performUndo() calls clearInterval(timer) immediately, so as long as the click handler fires before the interval's own callback for that tick, the undo wins. For a production system, guard the actual delete call server-side against a request that arrives after undo was already processed.

A CSS @keyframes animation runs on the compositor thread and updates every frame (60fps), giving a perfectly smooth shrinking bar, whereas updating width from a 1-second JS interval would produce a visibly stepped, jumpy bar.

Update both the animation-duration in the CSS @keyframes shrink rule and the secondsLeft starting value in JavaScript to the same new number — they are two independent timers that must be kept manually in sync.

Yes — it has role="status" and aria-live="polite", so its appearance and content are announced automatically without moving keyboard focus away from wherever the user currently is.

Yes — the pattern is identical for one item or many; just update the copy ("Delete this item?" instead of "Delete 7 items?") and the trigger logic that determines the count.