# A11Y-11 — Async state changes are announced and focus is managed — transient status via live regions without stealing focus; context replacements move focus to the revealed surface, never both for the same message

L1 · hybrid · Accessibility

**Fails when:** message announced by both a live region and a focus move · transient toast stealing focus

## Requirement

Every async state change communicates through exactly one of two channels, chosen by
what the change *is*:

- **Transient status** (saving indicator, success toast, background update): a live
  region (`role="status"` / `aria-live`) that announces without moving focus.
- **Context replacement** (a panel replacing the acting surface, an error that needs
  action): move keyboard focus to the revealed surface or its first actionable
  element. The focused element's content is voiced by the focus move itself — do
  NOT also mark it `role="alert"`, or the same message announces twice.

Never both channels for the same message; never neither.

## Rationale

Merges the accessibility checklist AA "Status messages" (announceable without focus) with the
focus-movement gap caught twice in loop runs 1–2 (post-submit focus stranded on a
hidden control; then a banner with `role="alert"` + programmatic focus announcing
twice). The two failures are one design decision made badly: which channel does this
state change use? This control forces the choice at plan time — CMP-3's state
enumeration is where each state declares its channel.

## Passes when

- Toast "Attendance submitted." is `role="status"`, auto-dismissing, focus untouched.
- On submit success that hides the acting button, focus moves to the revealed
  panel's first actionable element.
- An error banner that requires action receives focus (`tabindex="-1"` + `.focus()`)
  and carries NO `role="alert"` — the focus move is the announcement.
- A background auto-save indicator updates a polite live region only.

## Fails when

- Focus rests on a hidden/removed control after a state change (stranded focus).
- The same message is announced twice (live-region role AND programmatic focus).
- A state change is visually obvious but silent to AT (no live region, no focus).
- Focus is yanked for a transient message the user didn't act on (focus theft).

## How to verify

Deterministic half — planned `checks/async-states` extension: for each state surface,
assert (live-region role XOR focus-target wiring). Until built: verify manually via
DOM inspection (`document.activeElement` after each transition) and label it so.
Judgment half — the evaluator grades the channel *choice* per state: transient vs
context-replacement, and quotes any double-channel message.
