A form is not finished when the request succeeds. The interface must communicate completion and, when appropriate, give the user a safe way to reverse the action.
Prerequisites
You need a form with server or client validation, a rendering layer capable of changing the page title and main heading after navigation, and a way to render an in-page status message. You also need a clear product decision about whether the submitted action is reversible.
Implementation decision
Represent validation, confirmation-required, submitted, undone, and undo-unavailable as separate outcomes. Do not reuse an error summary for success.
Choose the success presentation from the navigation pattern. When submit stays on the current page, return a short status message so the result can be announced without moving focus or changing documents. When submit leads to a new screen, provide the destination title and primary heading instead.
Then branch by reversibility. A reversible submission returns an explicit Undo control. Activating it restores the submitted item and makes a second Undo unavailable. An irreversible submission does not return Undo; it requires an affirmative confirmation before it completes.
Expected result
A blank field produces field-specific validation feedback. A valid in-place submission produces a concise completion status and, if reversible, an Undo action. A valid navigational submission produces a clear page title and H1. A permanent action waits for confirmation and never suggests that it can be undone.
Limitation
This model defines what the UI should render, but it does not guarantee how every browser and screen reader will announce a live region, title change, or heading. Validate the real rendered experience with the browser and assistive technologies your application supports.
Working example
export const proofBoundary =
'This model verifies the flow’s presentation contract. It does not prove how a particular browser or assistive technology announces a live region, title, or heading.';
function successPresentation(presentation, task) {
const message = `Task “${task}” submitted successfully.`;
if (presentation === 'page') {
return {
title: `Task submitted – Task manager`,
heading: 'Task submitted',
status: null,
message,
};
}
return {
title: null,
heading: null,
status: { role: 'status', text: message },
message,
};
}
/**
* Models a form submission UI. A real page can render `title` and `heading`
* after navigation, or render the `status` live region after an in-page submit.
*/
export function createTaskFormFlow({ presentation = 'status', reversible = true } = {}) {
if (!['status', 'page'].includes(presentation)) {
throw new RangeError('presentation must be "status" or "page"');
}
let pendingSubmission = null;
return {
submit({ task, confirmed = false } = {}) {
const normalizedTask = typeof task === 'string' ? task.trim() : '';
if (!normalizedTask) {
return {
ok: false,
kind: 'validation',
fieldErrors: { task: 'Enter a task.' },
// Success is not represented as a server-style error summary.
errorSummary: null,
};
}
if (!reversible && !confirmed) {
return {
ok: false,
kind: 'confirmation-required',
message: 'Confirm this permanent submission before continuing.',
};
}
pendingSubmission = { task: normalizedTask };
const feedback = successPresentation(presentation, normalizedTask);
return {
ok: true,
kind: 'submitted',
...feedback,
undoControl: reversible
? { action: 'undo', label: 'Undo submission', accessibleName: 'Undo submission' }
: null,
};
},
undo() {
if (!reversible || pendingSubmission === null) {
return {
ok: false,
kind: 'undo-unavailable',
status: null,
};
}
const { task } = pendingSubmission;
pendingSubmission = null;
return {
ok: true,
kind: 'undone',
restoredTask: task,
status: { role: 'status', text: `Submission of “${task}” was undone.` },
};
},
};
}
Read the full article: https://chriseugenerodriguez.com/blog/successful-form-submission-undo-accessibly