Confirmation and Undo
Match consequence to interruption, commit only the actual result, and keep recovery where people can still find it.
Choose the smallest sufficient interruption
| Consequence | Treatment | Evidence |
|---|---|---|
| Harmless reversible preference | Immediate action with visible state | Actual selected/applied value. |
| Reversible local removal | Clear action and persistent Undo/Restore | Local state changes and recovery restores it. |
| Consequential choice | Named AlertDialog with safe initial focus | Explicit consent to the described operation, not general authority. |
| Complex edit | Dialog/page with validation and retained draft | Actual saved result; failure preserves input. |
| Irreversible operation | Explain exact consequence and absence of recovery | Authorization and durable outcome outside the UI demonstration. |
Model the operation separately from the surface
- Opening a confirmation is not executing it. Cancel/Escape keep the object unchanged.
- Confirmation can begin a pending operation. Prevent duplicates, offer cancellation if it is real, and retain the request identity for reconciliation.
- A completed request commits its actual returned/persisted result. A failed or ambiguous request needs retry/reconciliation, not an invented success notification.
- Undo is an operation with its own pending/failure states. Do not offer it after the recovery window expires or when permissions no longer permit it.
- A dismissed message is not an undo. A modal close is not an abort. A green state is not evidence of language review or consent.
Working local consequence and recovery
Local example draft is present.
'use client';
import { useRef, useState } from 'react';
import { AlertDialog, AlertDialogBackdrop, AlertDialogClose, AlertDialogDescription, AlertDialogPopup, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, Button } from '@mobtranslate/ui';
export function ConfirmationContainmentExample() {
const [open, setOpen] = useState(false);
const [exists, setExists] = useState(true);
const [result, setResult] = useState('Local example draft is present.');
const cancel = useRef<HTMLButtonElement>(null);
const restore = useRef<HTMLElement>(null);
return <div data-confirmation-lab className="space-y-4">
<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialogTrigger disabled={!exists} render={<Button type="button" variant="destructive" />}>Remove local example</AlertDialogTrigger>
<AlertDialogPortal><AlertDialogBackdrop /><AlertDialogPopup initialFocus={cancel} finalFocus={() => exists ? true : restore.current} className={state => state.open ? 'local-confirmation-open' : ''}>
<AlertDialogTitle>Remove this local example?</AlertDialogTitle><AlertDialogDescription>This changes only the example on this page. A separate restore action remains available. No source, recording or database row will be deleted.</AlertDialogDescription>
<div className="flex flex-wrap gap-3 mt-6"><AlertDialogClose ref={cancel} render={<Button type="button" variant="outline" />}>Keep local example</AlertDialogClose><Button type="button" variant="destructive" onClick={() => { setExists(false); setResult('Local example removed. Use Restore local example to recover.'); setOpen(false); }}>Confirm local removal</Button></div>
</AlertDialogPopup></AlertDialogPortal>
</AlertDialog>
<Button ref={restore} type="button" variant="outline" aria-disabled={exists || undefined} onClick={() => { if (!exists) { setExists(true); setResult('Local example restored.'); } }}>Restore local example</Button>
<p role="status">{result}</p>
</div>;
}
Focus through each outcome
Focus the safe action on opening a consequential choice. On cancellation, return to the trigger. On a result that disables the trigger, return to an enabled restore or the next logical item. Keep recovery reachable even when a toast expires or is displaced by its display limit.
- Keep action labels specific and announce a concise actual result.
- Long explanations retain semantic paragraphs; a short connected description names the immediate consequence.
- Test both pointer and keyboard activation. Do not rely on automatic focus restoration when your result removed the invoking control.
Authority and persistence boundary
These examples operate on local English-only sample state. They do not prove deletion, ingestion, permission, review, publication or cultural authority. In a real application, retain operation receipts and source evidence and reconcile the durable result before describing it as complete.
Failure cases
- An outside click executing the consequence.
- Safe and destructive actions with indistinguishable names or accidental form submission.
- A restore action available only in an expired toast.
- A pending operation hidden by a closed surface without any result reconciliation.
- Focus sent to a disabled trigger after the object was removed.
Verification and release scope
- Verify the actual title, description relationships, focus destination, keyboard operation and task result in the consuming application.
- Exercise narrow and short viewports, both themes, long labels, text spacing, reduced motion and forced colors. Reach the last action by scrolling and keyboard.
- For overlays, check background interaction, scroll locking and each permitted dismissal path. A role or aria-modal attribute alone does not prove modal behavior.
- Use a named screen reader and browser for spoken output. Local examples and automated DOM checks do not prove announcements or a persisted backend result.
- Retain source/review/permission evidence. A container or confirmation cannot grant authority or establish that a request succeeded.