Snackbar with Undo — HTML CSS JS Snippet
Snackbar with Undo · Modals · Plain HTML, CSS & JS · Live preview
What's included
Features
pending object retains everything needed to undo before the window closes.deleteItem stores the nextElementSibling, so undo uses insertBefore to return the row to its precise slot — not the end of the list.transform: scaleX, a GPU-friendly transform, so the countdown stays smooth even under load.run class, reading offsetWidth, and re-adding it restarts the countdown on every snackbar, even back-to-back deletes.finalize first, committing the prior delete and re-targeting the snackbar — no lost or ambiguous undo state.setTimeout fires finalize when the window ends, making the delete permanent without any extra confirmation.sb-restore keyframe so users can see exactly which item returned.role="status" with aria-live="polite", so screen readers announce the deletion and the undo option.About this UI Snippet
Snackbar with Undo — Deferred Delete, Countdown Progress Bar & Exact-Position Restore

"Undo" is one of the kindest patterns in UI design. Instead of interrupting users with a "Are you sure?" confirmation dialog for every delete, you let the action happen instantly and offer a brief window to reverse it. That is the undo snackbar — popularised by Gmail and Material Design — and it makes destructive actions feel safe without nagging. This snippet implements the full pattern in plain HTML, CSS, and vanilla JavaScript: an instant delete, a snackbar with an animated countdown bar, an Undo that restores the item to its exact original position, and automatic commit when the window expires.
Deferred, reversible delete
The key insight is that the delete is *visually* immediate but *logically* deferred. deleteItem removes the row from the DOM straight away (so the list updates instantly) but stores everything needed to put it back in a pending object: the detached node itself, its former parent, and crucially its nextElementSibling — the element it sat before. That sibling reference is what lets undo restore the row to its precise original slot, not just append it to the end.
Countdown progress bar
The snackbar shows an sb-progress bar that scales from full to zero over the 4-second window using a pure-CSS transform: scaleX keyframe — a transform animation that runs on the compositor and stays smooth. Each time a snackbar appears, the bar's animation is restarted by removing the class, forcing a reflow with void offsetWidth, and re-adding it, so the countdown always plays fresh even on rapid successive deletes. The bar gives users a visible sense of how long they have to act.
Exact-position restore
undo clears the pending timer and reinserts the stored node: if the original next sibling still exists in the parent, it uses insertBefore to drop the row back exactly where it was; otherwise it appends. The restored row plays an sb-restore highlight animation so users can see what came back. After undo, pending is cleared so the action cannot be reversed twice.
Commit on expiry or on the next action
If the user does nothing, the setTimeout fires finalize, which simply clears pending — the node was already removed, so the delete becomes permanent. The same finalize is called at the start of deleteItem, so if you delete a second item while a snackbar is still showing, the first delete commits immediately and the snackbar re-targets the new item. This single-pending model avoids the bug where overlapping deletes lose track of what to undo.
Pair this with a swipe-to-delete list for the delete gesture, a toast notification system for non-undoable messages, or a todo widget as the list.
Build with AI
Build, Understand, Optimize, and Extend It With AI
You don't have to trace the deferred-delete lifecycle by hand. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why deleteItem stores nextElementSibling rather than an array index, or why showSnack forces a reflow with void bar.offsetWidth before re-adding the run class to the progress bar. The same assistant can help optimize it, for instance checking whether the single-pending model correctly handles a rapid sequence of five or six deletes without losing any of them, or whether the aria-live region announces too often during fast successive deletes. It is just as useful for extending the pattern: ask it to change pending from one object to a stack so multiple deletes can each be undone independently, wire finalize() up to a real DELETE fetch call instead of just clearing state, or add a keyboard shortcut (like Ctrl+Z) that triggers undo() while the snackbar is visible. Treat the code less like a finished artifact and more like a starting point for a conversation.
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:
Build a Material-style "undo snackbar" for deleting list rows in plain HTML, CSS, and JavaScript — no libraries.
Requirements:
- A list of rows, each with a delete button. Clicking delete must remove the row from the DOM immediately (instant, optimistic removal) while storing everything needed to restore it later: the detached DOM node itself, its original parent element, and specifically its original nextElementSibling reference (not a numeric index), so it can be reinserted at its exact original position even if other rows changed in the meantime.
- A snackbar element with role="status" and aria-live="polite" that slides up from the bottom of its container, showing the name of the just-deleted item and an Undo button.
- A progress bar inside the snackbar that visually counts down the undo window using a CSS transform: scaleX animation from full to zero over a fixed duration (e.g. 4 seconds) — animate transform, not width, so it stays compositor-smooth. Each time the snackbar reappears, restart this animation by removing its animation class, forcing a synchronous reflow by reading the element's offsetWidth, then re-adding the class — otherwise the browser will not replay an already-running CSS animation.
- Clicking Undo must cancel the pending auto-commit timer, reinsert the stored node using insertBefore relative to its stored next-sibling reference (falling back to appendChild if that sibling no longer exists in the parent), and play a brief restore highlight animation on the returned row.
- If the countdown timer expires without an undo, the delete becomes permanent — simply clear the pending state since the node is already removed from the DOM.
- If the user deletes a second row while a snackbar for a previous delete is still showing, immediately commit the first delete (finalize it, canceling its timer) and show a fresh snackbar targeting the new row — never allow two deletes to be pending undo at once.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
- 1Paste HTML, CSS, and JSA "Tasks" card appears with four rows, each with a trash icon that shows on hover.
- 2Delete a taskClick a trash icon — the row vanishes instantly and a dark snackbar slides up from the bottom reading "Deleted …".
- 3Watch the countdownA thin purple bar shrinks across the bottom of the snackbar over four seconds, showing your window to undo.
- 4Click UndoHit Undo before the bar empties — the task reappears in its exact original position with a brief highlight.
- 5Let it expireDo nothing and the bar runs out — the snackbar slides away and the delete becomes permanent.
- 6Delete several quicklyDelete another task while a snackbar is showing — the previous delete commits immediately and the snackbar re-targets the new item.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Keep the optimistic UI: remove the row immediately and store pending. Fire the real DELETE request inside finalize (on expiry/commit), not in deleteItem — that way Undo simply cancels the timer and no request is ever sent. If you prefer to delete on the server first, send the request in deleteItem and a restore request in undo; the optimistic approach avoids the extra round-trip.
An index can drift if other rows are added or removed while the snackbar is showing. A reference to the next sibling node restores the item relative to its actual neighbour, and undo checks that the sibling still belongs to the parent before using insertBefore, falling back to append — so restore stays correct even if the list changed.
Edit the DURATION constant (milliseconds) and the 4s in the sb-count CSS keyframe to match — the JS timer and the visual bar must use the same duration. Four to six seconds is the usual range: long enough to react, short enough not to block the workflow.
This snippet keeps a single pending slot: each new delete commits the previous one immediately and shows a fresh snackbar for the newest item. If you need to undo multiple deletes, change pending to a stack/array, render a count ("3 items deleted"), and restore them in reverse order on undo.
In React, keep the list in state and a pending item with its index in a ref; remove on delete, restore by splicing back on undo, and use a useEffect timeout to commit. In Vue, manage the list with ref and a setTimeout in the delete method. In Angular, hold the list and pending item on the component. The countdown bar and slide-in CSS port unchanged.