A practical accessible form-success flow with Undo

A practical accessible form-success flow with Undo

calendar_today ago • schedule2 min read
— Originally published at chriseugenerodriguez.com

Build a form flow with explicit success and recovery states

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

🔥 Join developers growing publicly
Share your knowledge, build in public, and grow your developer presence with a global community.

More Posts

What Should a Health Awareness Page Help a Reader Do Next?

WAQAS AHMAD - Nov 13

My First Flow with Kestra.io

Amara Graham - Feb 6

Should Exact Search Matches Always Come First?

WAQAS AHMAD - Oct 30

The Rowling-Galbraith Mystery: Testing Stylometry with Burrows’ Delta

nevmenandr - Oct 10

When Should Delete Offer Undo?

WAQAS AHMAD - Oct 26
chevron_left
827 Points • 38 Badges
Santa Monica, CA • chriseugenerodriguez.com
31Posts
0Comments
6Connections
10 years fe

Related Jobs

View all jobs →

Commenters (This Week)

12 comments
1 comment
1 comment

Contribute meaningful comments to climb the leaderboard and earn badges!