How to replace "Are you sure?" with Undo — 5 steps and a working setup

"Are you sure you want to delete?" — and everyone clicks Yes. Without reading. Because that question has been asked two hundred times this month, and two hundred times the answer was yes.
Then someone deletes the wrong file — and the dialog didn't save them. It catches the mistake "I clicked by accident", but not the mistake "I clicked deliberately, on the wrong row". The second one happens more often.
There's a sturdier approach: delete immediately, and offer a way back. Step by step.
Worth knowing: for actions on important or user-owned data, the WCAG accessibility standard (criterion 3.3.4) requires one of three things — the action is reversible, the data is checked, or there's a confirmation step. So "Undo" isn't a half-measure, it's a full-fledged option. The same text explicitly notes the standard does not require a confirmation for every save.
Step 1. Sort actions into three buckets
Not everything needs to be undoable, or you'll drown.
- Ordinary (unchecking a box, removing a favourite) — no dialog, no undo. People just click again.
- Noticeable (delete a task, an email, a photo) — this is where undo goes. Your main bucket.
- Irreversible (closing an account, paying, emailing a real customer) — confirmation stays, and not as a checkbox: ask them to type the project name. That's the only way to force reading.
From here on we work with bucket two.
Step 2. Make deletion reversible in the database
Undo is impossible if the row is physically gone. So instead of DELETE, set a flag:
ALTER TABLE tasks ADD COLUMN deleted_at timestamptz;
-- "delete"
UPDATE tasks SET deleted_at = now() WHERE id = $1;
-- undo
UPDATE tasks SET deleted_at = NULL WHERE id = $1;
Then walk through every query once, adding WHERE deleted_at IS NULL. A forgotten filter in one query is the classic failure of this approach: a deleted task suddenly resurfaces in search or in a counter.
If you have a database, this is the right path. Keeping "deleted" items in server memory doesn't work: restart the process and there's nothing left to undo.
Real deletion is done by a background job that clears anything flagged more than a month ago.
Step 3. Remove the item from the UI instantly
Undo only works if deletion feels immediate. No spinners on a list row: remove the item at once and send the request in the background. That's exactly optimistic UI, with all its rules — including the mandatory failure branch.
function deleteTask(task) {
setTasks(prev => prev.filter(t => t.id !== task.id)); // right away
showUndo(task); // step 4
api.softDelete(task.id).catch(() => {
setTasks(prev => [...prev, task]); // failed — put it back
showError('Couldn\'t delete');
});
}
Note the catch: we restore the item and say so. Without that, the app silently "loses" deletions.
Step 4. Show the undo window — and don't kill it with a timer
The panel "Task deleted — Undo" appears at the bottom or the top. Usually it's a toast, with one important amendment.
The moment it contains a button, an auto-timer becomes a problem: people read slower, reach for the mouse — and the panel is already gone with nothing left to restore. The accessibility standard requires that any time limit can be turned off, adjusted or extended.
Practically, three rules:
- keep it noticeably longer than a normal toast (7–10 seconds, not 3);
- pause the timer on mouse hover and on keyboard focus;
- give a Close button so it can be dismissed early.
The Undo control must be a real button, reachable from the keyboard — not clickable text.
Step 5. Decide when the real request goes out
Here's the fork, and reliability depends on it.
"Delay the request": wait 7 seconds, then send the delete; if Undo is pressed, simply don't send it. Upside: no deleted_at column needed. Downside: close the tab before the timer fires and the deletion never happens at all. It's gone from the UI, still in the database. Next visit, the "deleted" task returns by itself.
"Flag it immediately": set deleted_at right away; undo clears it. Upside: the state is honest on any device, tabs can be closed. Downside: you need an undo request and a column.
The second is sturdier — take it if you have a database. The first is fine for drafts and local lists.
What you end up with
Normal flow: someone hits Delete — the row vanishes instantly, a panel with a button appears below. Ten seconds later the panel leaves, the record is flagged deleted, and a month later it's physically gone.
Failure flow: they realise they deleted the wrong thing, press Undo — the row returns to its place in the list. Not a single modal window in the entire session.
It's essentially the same rollback you know from deployments, applied to one user action.
Does everything need undo?
No. Irreversible things (payments, closing an account) can't have it — those keep a confirmation with meaningful typed input. For trivia it's overkill.
How long should undo stay available?
In the UI, 7–10 seconds while the panel is visible. In the database, far longer: a month of flagged rows costs pennies and will save you the week someone writes "could you bring that project back, please".
What if several items are deleted at once?
One panel for the whole operation: "12 tasks deleted — Undo". Twelve panels in a row is a reliable way to have none of them noticed.
Short story-lessons, an agent simulator and daily practice — in our mobile app. Free.





