Confirming and Undoing Destructive Row Actions

Permalink to "Confirming and Undoing Destructive Row Actions"

A destructive row action — delete, archive, revoke, cancel an order — is the highest-stakes control in a data table. Two protections are common: a confirmation dialog before the action, or an undo after it. Each has an accessibility failure mode. Confirmations fail when they are vague (“Are you sure?”) or put focus on the destructive button, so an extra Enter deletes. Undo fails when it lives in a toast that disappears in four seconds, somewhere a keyboard or screen reader user cannot reach in time.

This page covers choosing between them and implementing each so that keyboard and screen reader users are as protected as everyone else. It belongs to row actions & context menus.

Spec reference

Permalink to "Spec reference"

SC 3.3.4 Error Prevention (Legal, Financial, Data) (Level AA) applies to pages that cause legal commitments or financial transactions, modify or delete user-controllable data in data storage systems, or submit test responses. At least one of: the submission is reversible, checked for errors with a chance to correct, or confirmed before finalising. Deleting records in a data application falls squarely under “delete user-controllable data”.

SC 2.2.1 Timing Adjustable (Level A) applies to time limits — an undo that expires is a time limit on the ability to reverse. SC 2.2.1’s exception for “real-time” events does not cover it; either make it long, let users extend it, or keep undo available without a timer.

SC 4.1.3 Status Messages covers the “Deleted. Undo” message. Dialogs follow SC 2.4.3 and SC 4.1.2.

Confirm, undo, or both? Decision tree for protecting a destructive row action based on whether it can be reversed and how costly an accidental action is. Confirm, undo, or both?Can the action be reversed by the application?Yes, cheaplyUndoact at once; offer undo with noshort timerYes, but costlyUndo + summaryact; undo reachable from apersistent placeNoConfirm firstdialog naming record andconsequence
Undo is kinder when it is possible; confirmation is required when it is not.

When to confirm — and when to offer undo

Permalink to "When to confirm — and when to offer undo"

Undo is better for the common case: archive, move, remove from list, soft delete. It does not interrupt the user’s flow, it satisfies SC 3.3.4 through reversibility, and it protects against mistakes the user only notices later.

Confirmation is required when the action is genuinely irreversible — permanent deletion, sending money, revoking access that cannot be re-granted — or when its effects spread beyond the row (deleting a customer deletes their invoices).

Both are reasonable for bulk actions: confirm “Delete 12 invoices?”, then offer undo.

The misapplication to name is a confirmation dialog with generic text — “Are you sure you want to continue?” — and the destructive button focused by default. Users of every kind learn to press Enter through generic dialogs; putting focus on “Delete” turns that habit into data loss.

Annotated code example

Permalink to "Annotated code example"
<!-- CONFIRMATION: names the record and the consequence -->
<dialog id="confirm-delete" aria-labelledby="cd-title" aria-describedby="cd-desc">
  <h2 id="cd-title">Delete invoice INV-1042?</h2>
  <p id="cd-desc">This permanently deletes the invoice and its 3 payment records.
     It cannot be undone.</p>
  <form method="dialog" class="actions">
    <!-- SC 3.3.4: safe choice focused by default -->
    <button value="cancel" autofocus>Keep invoice</button>
    <button value="delete" class="danger">Delete invoice</button>
  </form>
</dialog>
// UNDO: act at once, announce with the key, keep undo reachable
let lastUndo = null;

async function archiveRow(row) {
  const id = row.dataset.id;
  const target = focusTargetAfterDelete(row);      // decide before removing
  await api.archive(id);
  row.remove();
  target.focus();
  lastUndo = { run: () => api.unarchive(id).then(() => restoreRow(id)), label: `Archive of ${id}` };
  // SC 4.1.3: the message tells keyboard users how to undo without finding the toast
  status(`${id} archived. Press Control Z to undo.`);
  showToast(`${id} archived`, { action: 'Undo', onAction: undo });   // visible, persistent until dismissed
}

document.addEventListener('keydown', (e) => {
  const inText = e.target.closest('input, textarea, [contenteditable="true"]');
  if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'z' && !inText && lastUndo) {
    e.preventDefault(); undo();
  }
});

async function undo() {
  const u = lastUndo; lastUndo = null;
  await u.run();
  status(`${u.label} undone.`);                      // focus goes to the restored row
}
/* The toast persists until dismissed — no four-second timer (SC 2.2.1) */
.toast { position: fixed; inset-block-end: 1rem; inset-inline-start: 1rem; }

The undo key announced in the status message is the important part. A toast’s Undo button is fine for pointer users, but reaching it by keyboard means tabbing to a fixed-position element whose place in the tab order is unclear. A global Ctrl+Z that reverses the last row action — guarded so it never fires in text fields — gives keyboard users the same speed of recovery.

Keyboard & AT behaviour

Permalink to "Keyboard & AT behaviour"
Event Expected behaviour Announcement
Delete… from row menu Dialog opens; focus on “Keep invoice” “Delete invoice INV-1042?, dialog. This permanently deletes… Keep invoice, button”
Enter immediately Cancels; focus back to row menu trigger Trigger read
Tab, Enter Deletes; focus to next row “INV-1042 deleted.”
Archive (undoable) Row removed; focus to next row “INV-1042 archived. Press Control Z to undo.”
Ctrl+Z Row restored; focus on it “Archive of INV-1042 undone.”
Confirmation dialogs that protect and that do not Comparison of a generic confirmation dialog with focus on the destructive button against a specific dialog naming the record with focus on the safe option. Confirmation dialogs that protect and that do not✗ Generic confirmation"Are you sure?"Focus on OK — one Enter deletesConsequences unstated✓ Specific confirmation"Delete invoice INV-1042?"Focus on Keep invoiceStates what else is deleted
Specific words and a safe default focus are the whole of the protection.

Integration context

Permalink to "Integration context"

Focus after the action follows preserving focus when rows are deleted; after undo, focus goes to the restored row. Confirmation dialogs should be native <dialog> elements, as covered in native dialog versus custom focus traps.

Cell-level edits have their own undo expectations, covered in undo and cancel affordances for inline cell edits. Use one undo stack for both, so Ctrl+Z means the same thing everywhere in the grid.

An undoable archive, from keypress to recovery Timeline of archiving a row by keyboard, hearing the undo instruction, continuing to work, and undoing with Control Z. An undoable archive, from keypress to recoveryArchiverow removed, focus to next rowStatus"Archived. Press Control Z to undo."Keep workingread the next rowsCtrl+Zarchive reversedFocuson the restored rowan undoable destructive action
Undo stays available until the user does something else destructive — not until a timer runs out.

Gotchas

Permalink to "Gotchas"

Toasts with role="alert" and a timer. An assertive, self-dismissing toast interrupts speech and then vanishes before a keyboard user can reach its button. Use a polite status message for the announcement and keep the visible toast until dismissed.

Undo after navigation. If the user leaves the page, the undo should either persist (a “Recently archived” view) or be clearly lost. Silent expiry is the worst option.

Bulk undo. One Ctrl+Z should reverse the whole bulk action, not one row of it.

Design system notes

Permalink to "Design system notes"

Provide an action framework rather than ad hoc handlers: each destructive action declares whether it is reversible, its confirmation copy template (“Delete {type} {id}?”), and its inverse. The framework then chooses confirm or undo, generates specific dialog text, focuses the safe option, manages the undo stack and the Ctrl+Z binding, and announces results through the shared status region.

Testing checklist

Permalink to "Testing checklist"

FAQ

Permalink to "FAQ"
Should destructive actions use a confirmation dialog or undo?

Undo when the application can reverse the action — it protects users without interrupting them. A confirmation when the action is irreversible or affects more than the row. SC 3.3.4 accepts either reversibility or confirmation.

Which button should be focused in a delete confirmation?

The safe one — Cancel, or better a specific “Keep invoice”. Focusing the destructive button turns an accidental Enter into data loss.

Is an undo toast accessible?

Only if keyboard and screen reader users can use it in time. Announce the action with the undo key in a polite status message, offer a keyboard shortcut for undo, and keep the toast until it is dismissed rather than on a short timer.

Does a disappearing undo button fail WCAG?

A short undo window is a time limit, which SC 2.2.1 requires to be adjustable. Keeping undo available until the next destructive action, or until dismissed, avoids the problem.

Permalink to "Related"

← Back to Row Actions & Context Menus