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>

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

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.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Ask an AI assistant to explain the race condition between the Undo button click and the countdown's final interval tick, and how a production implementation should guard against it on the server side rather than relying solely on client-side timing. It's also worth asking for a version that queues multiple undoable bulk actions (so undoing a second delete doesn't cancel the first one's window), or one that persists the pending-delete state across a page reload using sessionStorage.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a two-stage bulk-delete confirmation pattern in HTML, CSS and vanilla JavaScript: an explicit confirm dialog followed by an undo toast with a live countdown — no external libraries.

Requirements:
- A trigger button that opens a modal confirm dialog (role="alertdialog", aria-modal, aria-describedby) asking the user to confirm deleting several selected items, with Cancel and a destructive Delete button.
- On confirm, hide the dialog and show a separate "undo toast" fixed near the bottom of the screen (role="status", aria-live="polite") containing a summary of what was deleted, a visible numeric countdown in seconds, an Undo button, and a shrinking progress bar showing time remaining.
- The shrinking progress bar must be driven by a CSS keyframe animation matching the same duration as a JavaScript setInterval-based countdown, so the bar animates smoothly regardless of the timer's tick rate.
- If the countdown reaches zero without the user clicking Undo, finalize the deletion (call a distinct finalize function) and hide the toast.
- If the user clicks Undo before the countdown ends, cancel the pending deletion, stop the timer, and hide the toast — with no chance of the deletion also finalizing afterward.
- The confirm dialog must support closing via the Escape key and via a click outside the dialog, and auto-focus its primary destructive action button when it opens.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

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.