# Forms replaced by a thank-you that is never announced

Source: https://easeweb.dev/learn/thank-you-focus
Topics: Live updates > Form results are announced (WCAG 4.1.3)
The fix: Either move focus to the thank-you as soon as it takes the form's place, giving it tabindex="-1" so a script can focus it, or keep the form on the page and write the thank-you into the status line under it.
Test with: screen reader, keyboard
References: https://www.w3.org/WAI/WCAG22/Understanding/status-messages.html https://www.w3.org/WAI/WCAG22/Techniques/failures/F103 https://www.w3.org/WAI/tutorials/forms/notifications/ https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/tabindex https://html.spec.whatwg.org/multipage/infrastructure.html#node-remove-focus-fixup

This is a recreated teaching example, not a finding about a named client. The code is a starting pattern: test it in your own product. No completed assistive-technology test is claimed here.

## What happens

A bookshop's footer has a newsletter sign-up: "Sign up for our newsletter", an Email address field and a Subscribe button. The form sends in the background, and the shop already announces failures in a status line under it. Success is drawn differently: the field and the button disappear, and the thank-you takes their place, "Thanks! Check your inbox to confirm." It looks tidy, and it says the job is done. A screen reader user presses Subscribe and hears nothing. The button they were on no longer exists, so focus has nowhere to be, and when they move on they may find themselves somewhere else on the page, with no word on whether the sign-up worked.

## Why it fails

"Thanks! Check your inbox to confirm." is a status message unless it takes focus. Success Criterion 4.1.3 asks that a status message can be found by assistive technology through its role or properties, so that it is announced without focus moving. Content that takes focus is outside the criterion, because taking focus is a change of context and the content is read as focus lands, so that is a way to pass too. Here the thank-you does neither. It is a new `p` with no role, written into no live region, and nothing focuses it, which is failure F103. Meanwhile the Subscribe button that had focus is removed with the form. The HTML standard's focus fixup then leaves focus on the page's body, and nothing new is given to the screen reader to read there.

## Who is affected

The thank-you is drawn where the form was, so anyone looking at the footer sees it. The people who miss it are those who rely on what is spoken or shown in braille, and they also lose the control they were on.

## What automation and AI miss

A scan finds a labelled field, a named button and a status line, and passes the form. It never sends the form, since on a live site that would sign someone up, so it never sees the form swapped for the thank-you. EaseWeb's audit raises a lead for a page with a form and no live region at all; this page has the status line for failures, so it doesn't get that lead, and no check can tell from the markup that the form will be replaced. AI assistants asked to make the thank-you accessible may add `role="status"` to the new paragraph as it is created, which arrives together with its words and may not be read, or wrap the form in a live region, which gets the thank-you read but still leaves focus on the page's body. Only sending the form with a screen reader running shows what is said and where focus goes.

## Before: a thank-you that replaces the form

```html
<section aria-labelledby="signup-title">
  <h2 id="signup-title">Sign up for our newsletter</h2>
  <form id="signup">
    <label for="signup-email">Email address</label>
    <input id="signup-email" name="email" type="email" autocomplete="email" required>
    <button>Subscribe</button>
  </form>
  <p id="signup-message" role="status"></p>
</section>
```

```js
const signup = document.getElementById('signup');
const message = document.getElementById('signup-message');

// Empty the line, then write a moment later, so the same words written twice are still a change.
function announce(text) {
  message.textContent = '';
  setTimeout(() => (message.textContent = text), 100);
}

signup.addEventListener('submit', async (event) => {
  event.preventDefault();
  try {
    await subscribe(signup.email.value);
    const thanks = document.createElement('p');
    thanks.textContent = 'Thanks! Check your inbox to confirm.';
    signup.replaceWith(thanks);
  } catch {
    announce('Sorry, we could not sign you up. Please try again.');
  }
});
```

Failures are announced: they go into the status line, and the form and its focused button stay. Success is the gap. The new paragraph has no role and nothing focuses it, so it isn't read, and `replaceWith()` removes the Subscribe button that had focus, so focus falls to the page's body.

## After: a thank-you that takes focus

```html
<section aria-labelledby="signup-title">
  <h2 id="signup-title">Sign up for our newsletter</h2>
  <form id="signup">
    <label for="signup-email">Email address</label>
    <input id="signup-email" name="email" type="email" autocomplete="email" required>
    <button>Subscribe</button>
  </form>
  <p id="signup-message" role="status"></p>
</section>
```

```js
const signup = document.getElementById('signup');
const message = document.getElementById('signup-message');

// Empty the line, then write a moment later, so the same words written twice are still a change.
function announce(text) {
  message.textContent = '';
  setTimeout(() => (message.textContent = text), 100);
}

signup.addEventListener('submit', async (event) => {
  event.preventDefault();
  try {
    await subscribe(signup.email.value);
    const thanks = document.createElement('p');
    thanks.tabIndex = -1;
    thanks.textContent = 'Thanks! Check your inbox to confirm.';
    signup.replaceWith(thanks);
    // The button that had focus is gone: give focus to what took its place.
    thanks.focus();
  } catch {
    announce('Sorry, we could not sign you up. Please try again.');
  }
});
```

The design stays as drawn. The thank-you gets `tabindex="-1"`, which lets a script focus it without adding it to the Tab order, and it is focused as soon as it is in the page. A screen reader reads it as focus lands, and the next Tab goes on from there, to whatever follows the form, so nobody loses their place. Failures are unchanged: the form stays, focus stays on Subscribe, and the status line says what went wrong.

## After: keep the form, and write the thank-you into the status line

```html
<section aria-labelledby="signup-title">
  <h2 id="signup-title">Sign up for our newsletter</h2>
  <form id="signup">
    <label for="signup-email">Email address</label>
    <input id="signup-email" name="email" type="email" autocomplete="email" required>
    <button>Subscribe</button>
  </form>
  <p id="signup-message" role="status"></p>
</section>
```

```js
const signup = document.getElementById('signup');
const message = document.getElementById('signup-message');

// Empty the line, then write a moment later, so the same words written twice are still a change.
function announce(text) {
  message.textContent = '';
  setTimeout(() => (message.textContent = text), 100);
}

signup.addEventListener('submit', async (event) => {
  event.preventDefault();
  try {
    await subscribe(signup.email.value);
    announce('Thanks! Check your inbox to confirm.');
  } catch {
    announce('Sorry, we could not sign you up. Please try again.');
  }
});
```

The form is no longer replaced. The thank-you goes into the status line that already carries failures, so the screen reader reads it when it has finished speaking, and focus never leaves Subscribe. This changes the design: the field and the button stay on screen after a sign-up, with the thank-you under them. In return, people can see which address they used, correct it, or sign up another one. This is the fix in [form success and error messages that are never announced](/learn/form-results).

## Which option to use

Both get the thank-you read and leave nobody on a removed button. Move focus to the thank-you when the design must replace the form: it is two lines of script, and the page looks exactly as drawn. Keep the form when the design can change: focus never moves, one status line handles both results, and the address stays in view to be checked. Neither is better for every site; it depends on whether the form has to go.

## Implementation decisions

**Focus the whole message.** Give focus to the element that holds the result in words, not to an icon, a heading that only says "Done", or the top of the page. Keep the thank-you short enough to be read in one go; if it needs a heading, make the heading say the result ("You're signed up") and focus the heading.

**After the new markup renders.** In a component framework, the success branch often swaps the form inside an `{#if}` or redraws the whole footer, which removes the focused button just as `replaceWith()` does. Move focus once the new content is in the page (after Svelte's `tick()`, React's effect, Vue's `nextTick()`), and check that a later redraw doesn't replace the element you focused.

**The focus ring.** After keyboard use, browsers usually draw a focus ring around the focused paragraph. Leave it: it shows sighted keyboard users where they are now. It usually doesn't appear after a mouse click.

**Failures stay put.** A failed send must not replace the form, since people need it to try again. Keep focus on Subscribe and write the failure into the status line, as both fixes do. [Form success and error messages that are never announced](/learn/form-results) explains that line, and why it is emptied before each message.

**Embedded forms.** Forms from mailing-list and marketing services often bring a script that swaps the form for their own thank-you. Test the embed on your page; if its script can't be changed, check whether the service can redirect to a thank-you page instead, which a screen reader announces as a new page.

**Not fixes.** Adding `role="status"` or `aria-live` to the new paragraph as it is created gives a region that arrives together with its words, which is often not announced; [status messages that are shown but never announced](/learn/status-messages) shows that failure. Wrapping the form in a live region and swapping it inside gets the thank-you read, but focus still falls to the page's body.

## Verify the fix

1. With a screen reader on, send the form with a valid address: the thank-you is read once, either as focus lands on it, or from the status line with focus still on Subscribe.
2. Press Tab: focus goes to the control after the form, or to the next control after Subscribe, never back to the top of the page; using the keyboard, focus is visible on the thank-you or on Subscribe.
3. Go offline in developer tools and send: the form stays, the failure is announced from the status line, and focus stays on Subscribe.
4. On the real page, send the form and check `document.activeElement` in the console: it is the thank-you or the Subscribe button, never `body`.

Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat after the form component, its script or the embed changes.

## Limits

Screen readers and browsers differ in what they read when focus moves to a paragraph, and in where they continue after an element is removed. This pattern covers one short form whose success replaces it in place. A form that stays on the page and announces its result is covered in [form success and error messages that are never announced](/learn/form-results), and a form that goes to a new page or view in [single-page app route changes that are never announced](/learn/route-changes). Test your real forms, including embedded ones, with the screen readers your users use.

---

Get this guide in your coding agent: MCP https://easeweb.dev/mcp (setup at https://easeweb.dev/services/developers)
