Notifications & live regions
Pick the right channel (inline validation, MessageBar, Toast or Dialog) and make sure screen readers announce the message at the right time and level.
Choose a notification channel
The level of interruption should match the importance. Start with the least disruptive channel that users will still notice.
| Channel | When | Interruption | Screen reader |
|---|---|---|---|
| Inline validation (Field) | An error or hint tied to one input. | Low | Read when the field gets focus (aria-describedby). |
| MessageBar | State of a page / region: quota warnings, read-only mode, data load errors. Stays until resolved. | Medium | role=alert (error, warning) or role=status (info, success). |
| Toast | Short feedback for the action just taken: saved, sent, copied. Does not block anything. | Low | Radix notification region; press F8 to reach it. |
| Dialog modalType="alert" | A decision is needed now, usually destructive or irreversible. | High (blocking) | role=alertdialog, focus moves to the surface. |
| Live region (AriaLiveAnnouncer) | Changes with no visible message: filtering done, more items loaded, result counts. | None | Read only, nothing is shown. |
{/* An error tied to one field */}
<Field label="Discount code" validationState="error" validationMessage="The code expired on September 30.">
<Input defaultValue="SAKURA30" />
</Field>
{/* State of the whole page / region that stays until it is resolved */}
<MessageBar
intent="warning"
title="Storage is almost full."
actions={<Button size="small">Upgrade</Button>}
>
You have used 92% of your storage.
</MessageBar>
{/* Short feedback for the action just taken */}
toast({ intent: "success", title: "Changes saved" });
{/* A decision is needed now; every other action is blocked */}
<DialogSurface modalType="alert">
<DialogTitle action={null}>Delete project “Sakura”?</DialogTitle>
…
</DialogSurface>polite & assertive
A live region decides when the screen reader reads a change: polite waits until the user is idle, assertive interrupts right away.
| Level | Attributes | Yugen components | Used for |
|---|---|---|---|
| Polite | role="status" · aria-live="polite" | MessageBar info / success; Toast info / success; announce() by default | Confirmations, results, progress. |
| Assertive | role="alert" · aria-live="assertive" | MessageBar error / warning; Toast error / warning; announce(…, { politeness: "assertive" }) | Errors that block work, data loss, session expiry. |
{/* Polite: read when the screen reader is idle; results, status, confirmations */}
<div role="status">Draft saved at 2:05 PM</div>
{/* Assertive: interrupts right away; only for errors and warnings that need action */}
<div role="alert">Connection lost. Changes are not saved.</div>
{/* The live region must be in the DOM BEFORE its content changes */}
<p role="status" className="sr-only">{message}</p>Some screen reader and browser combinations ignore a live region that is inserted into the DOM at the same time as its content. For a MessageBar that appears after an action, keep a container that is always present and only replace what is inside it, or also call announce(), so it is read.
Don't auto-dismiss what matters
A toast closes after 3 seconds by default. Slow readers, people using screen magnification and screen reader users can all miss it (WCAG 2.2.1).
import { toast } from "@/components/ui";
// Success with no action: closing after 3 seconds is fine.
toast({ intent: "success", title: "Link copied" });
// Error or an action: keep it until the user closes it.
toast({
intent: "error",
title: "Couldn't sync",
body: "Check your network connection and try again.",
timeout: -1,
action: {
label: "Retry",
// Screen reader users may not reach the toast in time; describe another way to do it.
altText: "Sync again with the Sync button in the toolbar",
onClick: retrySync,
},
});- Hovering or focusing a toast pauses the timer; F8 moves focus to the notification region.
- An action in a toast must also be available elsewhere (a button on the page, a menu);
altTexttells screen reader users where. - Errors that block work belong in a MessageBar or inline validation, not in a toast.
Async results
When content changes without a visible message (filtering a list, loading more, autosave), read the result through a live region.
10 results
- Tokyo
- Kyoto
- Osaka
- Seoul
- Singapore
- Hanoi
- Da Nang
- São Paulo
- Zürich
- Montréal
The example above uses a <p role="status" className="sr-only"> that updates 600ms after the user stops typing. For many messages from different places, use the AriaLiveAnnouncer utility with useAnnounce(): its queue spaces messages out so none is dropped, and batchId replaces an older message of the same kind with the latest one.
import { AriaLiveAnnouncer, useAnnounce, YugenProvider } from "@/components/ui";
// 1. Add once at the root: renders two hidden live regions (polite / assertive)
<YugenProvider applyToDocument>
<Toaster />
<AriaLiveAnnouncer>
<App />
</AriaLiveAnnouncer>
</YugenProvider>
// 2. Call it anywhere
function UploadButton({ files }: { files: File[] }) {
const { announce } = useAnnounce();
async function upload() {
announce("Uploading…");
try {
await uploadFiles(files);
announce(`Uploaded ${files.length} files`, { batchId: "upload" });
} catch {
announce("Upload failed. Try again later.", { politeness: "assertive", batchId: "upload" });
}
}
return <Button onClick={upload}>Upload</Button>;
}Do & don't
- Add an icon and text to every state; don't rely on color alone.
- Write messages that say what happened and what to do next.
- One event, one message; combine rapid updates with batchId or a debounce.
- Keep error toasts and toasts with an action until the user closes them (timeout: -1).
- Using role="alert" for routine information. It interrupts the user.
- Announcing on every keystroke.
- Putting the only copy of information or an action in an auto-closing toast.
- Using a Dialog for a message that needs no decision.