Yugen UI

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.

ChannelWhenInterruptionScreen reader
Inline validation (Field)An error or hint tied to one input.LowRead when the field gets focus (aria-describedby).
MessageBarState of a page / region: quota warnings, read-only mode, data load errors. Stays until resolved.Mediumrole=alert (error, warning) or role=status (info, success).
ToastShort feedback for the action just taken: saved, sent, copied. Does not block anything.LowRadix 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.NoneRead only, nothing is shown.
Inline validation
The code expired on September 30.
MessageBar
Toast
Alert dialog
{/* 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.

LevelAttributesYugen componentsUsed for
Politerole="status" · aria-live="polite"MessageBar info / success; Toast info / success; announce() by defaultConfirmations, results, progress.
Assertiverole="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); altText tells 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

Do
  • 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).
Don't
  • 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.