Accessible Notifications & Toasts
A toast asks a screen reader user to hear a message and act on it before it slides away, and those two demands pull against each other. This guide builds notifications everyone can perceive: the live region that has to exist before the message, the role="status" versus role="alert" politeness fork, why auto-dismiss collides with Timing Adjustable, why a toast must never take focus, stacking without flooding, and status that never rides on color alone. Copy-ready HTML, JavaScript, and React mapped to WCAG 2.2.
Decide the Pattern Before You Write the Markup
Notifications go wrong before a single line of ARIA is written, because teams reach for one component, the toast, to carry every kind of message: a quiet confirmation, an urgent error, a time-limited offer, an action the user is meant to take. Those are not the same job, and forcing them into a corner box that fades on a timer is what makes so many notification systems inaccessible. Two questions sort out which pattern you actually need, and almost every other decision in this guide follows from the answers.
The first question is about urgency: can this message wait for the screen reader to finish its current sentence, or must it interrupt? A saved draft or an updated result count can wait, so it belongs in a polite live region. A form submission that failed, or a session about to expire, cannot wait, so it belongs in an assertive one. That is the politeness fork, and it decides between role="status" and role="alert".
The second question is about whether the user has to do something. Is the message fire and forget, like “Draft saved”, or does it carry an action or essential information, like an Undo button, an error to correct, or a warning to act on? Fire-and-forget messages can be toasts that auto-dismiss. Anything the user has to read and act on is not a fire-and-forget toast at all, and putting it on a disappearing timer is the single most common notification failure.
The two questions that decide the pattern
Ask can it wait? and must the user act on it? If it can wait and needs no action, it is a toast: a polite live region that announces without taking focus and may auto-dismiss. If it cannot wait, promote it to an assertive region so it interrupts. If the user has to act on it, it does not belong on a timer: keep it in a persistent alert banner, or, if it must block the task until answered, an alert dialog. A sighted mouse user might catch a message that flashes and fades; a keyboard or screen reader user needs it to stay long enough to hear and reach.
Those two questions map onto three components. A toast is transient and non-modal, announces through a polite region, and never takes focus. An alert banner is persistent and sits in the flow of the page until dismissed or resolved, which suits an error the user must fix. An alert dialog is modal: it interrupts, takes focus, and blocks the rest of the page until answered, and it is built with the dialog pattern, not the toast pattern. The rest of this guide is mostly about building the toast correctly, because that is where the subtle bugs live, but the escape hatch is always there: when a message needs a response, stop reaching for a toast.
How Notifications Map to WCAG 2.2
No criterion says “you must add toasts,” but the moment the page changes without a reload, one criterion is written for exactly that situation. The highlighted row, 4.1.3 Status Messages, is the defining one: a change has to be announced to assistive technology without moving focus. Close behind it is 2.2.1 Timing Adjustable, the criterion that governs anything auto-dismissing on a timer. The rest of the table covers the controls inside a toast, keyboard access, color, contrast, reflow, and not obscuring what has focus.
| Criterion | Level | How it applies to notifications and toasts |
|---|---|---|
| 4.1.3 Status Messages | AA | The notification is announced through a live region so a screen reader speaks it without the user's focus ever moving to it. |
| 2.2.1 Timing Adjustable | A | A toast that auto-dismisses sets a time limit on its content, so anything the user must read or act on has to be pausable, extendable, or free of a timer entirely. |
| 4.1.2 Name, Role, Value | A | The dismiss control and any action inside the toast are real buttons with accessible names, not bare glyphs or clickable divs. |
| 2.1.1 Keyboard | A | A keyboard user can reach and operate the dismiss button and any action, and can do so before the toast disappears. |
| 1.4.1 Use of Color | A | Whether a message is a success or an error is carried by text or a labeled icon, not by a green or red background alone. |
| 1.4.3 Contrast (Minimum) | AA | Toast text meets 4.5 to 1 against its own background, which is easy to miss when a tinted success or error surface sits behind pale text. |
| 2.4.11 Focus Not Obscured | AA | A fixed-position toast does not cover the control that currently has keyboard focus, so the user can still see what they are operating. |
| 1.4.10 Reflow | AA | At 320 pixels wide the toast reflows into the viewport and does not force horizontal scrolling or clip its text and controls. |
Each criterion links to its full reference and interactive demo. The complete WCAG 2.2 criteria are one click away.
1. The Live Region Has to Exist Before the Message Does
This is the bug that silently breaks more toasts than any other, and it has nothing to do with the visible design. A screen reader announces a live region by watching an element that is already in the accessibility tree and speaking whatever text appears inside it. If your code creates a fresh element that carries both the aria-live attribute and the message text in the same operation, the region is usually not being observed yet, and the message is never spoken. The toast slides in, the sighted user sees it, and the screen reader says nothing.
The fix is to separate the container from the content. Put an empty live region into the initial HTML so it is present on page load, and then set or append text into it when an event happens. The container is what has to pre-exist; the message is the change the screen reader reacts to.
<!-- In the initial HTML: present and EMPTY on load.
This one region is the announcement layer for every toast.
aria-atomic="false" so only the newly added toast is read,
not the whole stack, when several are on screen. -->
<div
id="toast-region"
aria-live="polite"
aria-atomic="false"
class="toast-region"
></div>const region = document.getElementById("toast-region")
function showToast(message) {
// The region already exists, so appending a child is a change
// the screen reader announces. Never create the region in here.
const toast = document.createElement("div")
toast.className = "toast"
toast.textContent = message
region.appendChild(toast)
return toast
}
showToast("Draft saved.")Two more details matter. Never hide the region with display:none or visibility:hidden, because that removes it from the accessibility tree and nothing inside it is announced; if the announcement layer is visually separate from your toasts, hide it with a visually hidden or .sr-only class that keeps the element rendered. And do not flood the region with rapid updates, such as an announcement on every one percent of an upload, which turns the screen reader into a stutter; debounce to meaningful milestones. The 4.1.3 Status Messages reference covers the live-region mechanics in depth; this guide builds the notification component on top of them.
2. role="status" vs role="alert" vs role="log": The Politeness Fork
Once the region exists, its politeness decides how the announcement reaches the user. There are two live-region roles you will reach for most, plus a third for history, and each is a shorthand for an aria-live value you could also set by hand.
| Role | Implicit politeness | Use it for |
|---|---|---|
| role="status" | aria-live="polite" | Confirmations, result counts, progress. The screen reader finishes its sentence, then reads it. This is your default. |
| role="alert" | aria-live="assertive" | Errors and time-critical warnings only. It interrupts whatever is being spoken, so use it sparingly. |
| role="log" | aria-live="polite" | A running history where order matters, such as a chat transcript or an activity feed. New entries are added and read in sequence. |
The temptation is to make everything assertive so nothing is missed. Resist it. Assertive is an interruption, and an interface that cuts the screen reader off on every save, every filter change, and every minor update is exhausting to use even though it technically announces. Default to role="status", and promote a message to role="alert" only when it genuinely cannot wait: a failed submission, a lost connection, a session about to time out. A practical setup is to keep two regions in the DOM at all times, one polite and one assertive, and route each message to the right one.
<!-- Polite: waits its turn. Most toasts live here. -->
<div role="status" class="sr-only" id="polite-region"></div>
<!-- Assertive: interrupts. Errors and urgent warnings only. -->
<div role="alert" class="sr-only" id="assertive-region"></div>Announcing the same message twice
A live region only announces text that has changed. If a user clicks Copy twice, setting the region to “Copied” a second time does nothing, because the text is identical to what is already there. The reliable fix is to clear the region and then set the text again as a separate change, with a beat in between so the browser registers two distinct updates.
// Clear, then set on the next frame, so an identical message
// is seen as a change and re-announced.
function announce(region, message) {
region.textContent = ""
requestAnimationFrame(() => {
region.textContent = message
})
}
announce(politeRegion, "Copied to clipboard.")Wrap this in a small helper and use it everywhere a confirmation can repeat. It is the difference between a Copy button that reassures the user each time and one that goes quiet after the first click.
3. Auto-Dismiss and the WCAG 2.2.1 Trap
The defining feature of a toast, that it disappears on its own, is also where it collides with accessibility. Removing content on a timer is a time limit, and 2.2.1 Timing Adjustable requires that time limits on content be adjustable unless they are essential. Whether a given toast triggers that criterion depends entirely on the second question from the top of this guide: does the user have to read or act on it?
A pure confirmation is the safe case. “Draft saved” has already been spoken by the live region the instant it appeared, so when the visual fades a few seconds later, nothing is actually lost. That kind of toast can auto-dismiss. Even then, do two things: pause the timer while the pointer is over the toast and while keyboard focus is inside it, so a returning user is not fighting the clock, and always provide a manual close.
// A confirmation toast may auto-dismiss, but pause the timer on
// hover AND on keyboard focus, so it never vanishes while someone
// is reading it or reaching into it.
function autoDismiss(toast, ms) {
let timer = window.setTimeout(() => toast.remove(), ms)
const pause = () => window.clearTimeout(timer)
const resume = () => {
timer = window.setTimeout(() => toast.remove(), ms)
}
toast.addEventListener("mouseenter", pause)
toast.addEventListener("mouseleave", resume)
toast.addEventListener("focusin", pause) // focus entered the toast
toast.addEventListener("focusout", resume)
}
// Do NOT call this on a toast that holds an action or an error.The line you cannot cross
A notification that carries an action or essential information must not auto-dismiss. An Undo button, an error the user has to fix, a warning they need to respond to: none of these should sit on a timer, because a keyboard or screen reader user often cannot read the message and reach the control before it is gone. If a toast has a button, either it never times out, or it is not really a toast and belongs in a persistent alert banner. When you catch yourself hunting for the perfect number of seconds to display an actionable toast, that is the pattern telling you it chose the wrong container.
There is no universally correct duration. Design systems suggest ranges from a few seconds up to around ten, but those are starting points that assume a short, non-actionable message and an average reader. Longer text needs longer, an action needs no timer at all, and pausing on hover and focus is what turns a guess into something fair.
4. A Toast Never Steals Focus
When a toast is silent for screen reader users, the instinct is to move focus to it so it gets read. That is the wrong fix, and it is worth being firm about. Moving focus rips the user out of whatever they were doing, loses their place in the form or list they were working through, and leaves focus stranded inside a box that is often about to disappear on a timer. The whole reason a live region exists is to announce a change without moving focus, and 4.1.3 requires exactly that. Keep focus where the user put it, and let the polite or assertive region speak.
The dismiss button is a real button
A toast that offers a close control needs a genuine <button> with an accessible name, not a <div> holding a times sign with an onClick. A div is not in the tab order and announces as nothing; a bare glyph gives the button no name. Because the visible content already says what the message is, name the button by its action and hide the decorative glyph.
<div class="toast" role="status">
<p class="toast-message">Draft saved.</p>
<!-- Real button, named by its action; the glyph is decorative. -->
<button type="button" class="toast-close" aria-label="Dismiss notification">
<svg aria-hidden="true" focusable="false" viewBox="0 0 20 20" width="20" height="20">
<path d="M6 6l8 8M14 6l-8 8" stroke="currentColor" stroke-width="2" fill="none" />
</svg>
</button>
</div>By convention, pressing Escape dismisses the most recent toast, which is a small courtesy for keyboard users clearing a stack. When a toast that held focus (because the user tabbed to its Undo or Close button) is dismissed, send focus somewhere sensible, usually back to the control that triggered the action, rather than letting it fall to the top of the page.
When it should take focus, it is a dialog
There is one case where a notification legitimately takes focus and blocks the page: when it must interrupt the task and force a response, like a confirmation before deleting an account. That is not a toast. It is an alert dialog, with role="alertdialog", a focus move into it, a focus trap while it is open, and focus restoration when it closes. Build it with the accessible dialog guide, and for the mechanics of moving and restoring focus, see the focus management guide. The rule stays clean: toasts announce, dialogs interrupt.
5. Stacking, Queueing, and Not Flooding
Real apps fire more than one notification at a time, and how you handle the pile-up matters as much as any single toast. Do not create a separate assertive region for each message; several assertive regions talking over each other is chaos. Keep one polite region as the announcement layer and append each new toast into it, and the screen reader reads the additions in order. Reserve the assertive region for the occasional error.
Visually, cap how many toasts are on screen at once. A tall column of ten stacked toasts is noise for everyone and a wall of tab stops if each has a control. Show a small number, queue the rest, and let them advance as earlier ones dismiss. If toasts persist long enough to be a group, give the stack a role such as regionwith an accessible name like “Notifications” so a screen reader user can find and review them, rather than relying on the single spoken announcement.
Position deserves care too. Toasts usually sit fixed to a corner, and a fixed element can cover the control the user just moved to with the keyboard, which fails 2.4.11 Focus Not Obscured. Keep the toast area clear of interactive content, or shift it when it would overlap what has focus. And because the toast is fixed, check it at 320 pixels wide: it should reflow to fit the viewport, wrap its text, and keep its close button reachable rather than spilling off the edge of a phone screen.
6. Color, Contrast, and Not Signaling Status by Color Alone
Notification systems lean hard on color: green for success, red for error, amber for warning. Color is a fine reinforcement, but it can never be the only thing that tells a user what kind of message this is. A color-blind user may not distinguish the green toast from the red one, and a screen reader announces the text, not the background, so a toast whose only difference is its color says the same thing to assistive technology whether it succeeded or failed. That is a 1.4.1 Use of Color failure.
The fix is to put the meaning in the content. Lead the message with a word like Success or Error, or pair the colored surface with an icon that has a text label, so the nature of the message survives without color. If you use an icon and the adjacent text already conveys the status, mark the icon aria-hidden="true" so it is not read twice.
<!-- The word "Error" carries the meaning; the color and icon
only reinforce it. Nothing depends on red alone. -->
<div class="toast toast-error" role="alert">
<span class="toast-icon" aria-hidden="true">!</span>
<p><strong>Error:</strong> Payment could not be processed.</p>
<button type="button" aria-label="Dismiss notification">...</button>
</div>Do not forget plain contrast either. Tinted success and error surfaces are often paired with pale text that looks fine to a designer but drops below the 4.5 to 1 contrast minimum. Check the toast text against its actual background, in both light and dark themes, not against white.
7. Notifications in React
React makes the pre-existing-region rule easy to get wrong, because the natural instinct is to render a toast component only when there is something to show. If the live region is inside that component, it mounts together with its text, and you are back to the silent-toast bug. The pattern that avoids it is a provider mounted once at the app root, whose live region is always in the tree; individual toasts are appended into it as state changes.
// Mounted ONCE at the app root. The live regions are always in
// the tree, so appended toasts are announced in place.
function ToastProvider({ children }) {
const [toasts, setToasts] = useState([])
const notify = useCallback((message, options) => {
const tone = options && options.error ? "error" : "success"
const id = crypto.randomUUID()
setToasts((list) => [...list, { id, message, tone }])
}, [])
const dismiss = useCallback((id) => {
setToasts((list) => list.filter((t) => t.id !== id))
}, [])
return (
<ToastContext.Provider value={notify}>
{children}
{/* Polite region for confirmations. Present on every render. */}
<div className="toast-stack" aria-live="polite" aria-atomic="false">
{toasts
.filter((t) => t.tone === "success")
.map((t) => (
<Toast key={t.id} toast={t} onDismiss={dismiss} />
))}
</div>
{/* Separate assertive region for errors only. */}
<div className="toast-stack toast-stack-error" role="alert">
{toasts
.filter((t) => t.tone === "error")
.map((t) => (
<Toast key={t.id} toast={t} onDismiss={dismiss} />
))}
</div>
</ToastContext.Provider>
)
}Each Toastrenders its message with a leading Success or Error label, a real dismiss button named “Dismiss notification”, and, for confirmations only, a timer that pauses on mouseenter and focusin. Route errors to the assertive region and everything else to the polite one, so urgency is a property of the message rather than an afterthought.
You do not have to hand-build this. Libraries such as react-hot-toast, Sonner, and Radix Toast keep a persistent region mounted for you and handle the announcement plumbing, which is the part that is easy to break. What they cannot decide for you is the politeness of each message, the pause on hover and focus, the named dismiss control, the rule that an actionable toast does not auto-dismiss, and status that does not depend on color. Audit any toast library the same way you would audit your own: turn a screen reader on, trigger a toast, and listen. For the wider set of React patterns, the React accessibility guide covers announcing dynamic changes across the app, and the accessible AI chat guide works the streaming case where announcements arrive continuously.
8. Testing Notifications and Toasts
Notifications are one of the areas where automated tools help least, because the thing that matters, whether the announcement actually fires and whether the user can act in time, only shows up with a screen reader running and a keyboard in hand.
The screen reader pass
With NVDA, JAWS, or VoiceOver on, trigger each notification: save something, submit with an error, run a search, start an upload. Each change should be spoken once, without your focus moving, and without stuttering through half-words. Confirm the error interrupts (assertive is doing its job) while the routine confirmations wait their turn. Fire the same confirmation twice and check it re-announces. Let an auto-dismissing toast fade and confirm nothing essential was only in that toast.
The keyboard and timing pass
Trigger a toast that has an action or a close button and, using only the keyboard, reach and operate it before it disappears. If you cannot, the timer is too short or the toast should not be timed at all. Hover a toast and confirm its timer pauses; tab into it and confirm the same. Press Escape and confirm it dismisses. Then check the visual layer: at 320 pixels the toast reflows and its close button stays reachable, a fixed toast does not cover the focused control, the text meets contrast against its own background, and the success or error meaning survives with color removed.
Automated checkers earn their keep on the mechanical failures. axe and WAVE can flag a missing accessible name on the dismiss button, a clickable div, or contrast below the minimum. What they cannot tell you is whether the announcement actually fired, whether assertive is overused, or whether a keyboard user can reach the action before the toast vanishes. Those are manual judgments, and they are the ones that decide whether the notification works.
Common Notification Mistakes & How to Fix Them
These are the errors that turn up most in notification audits. Almost all of them trace back to two habits: treating a toast as the container for every kind of message, and forgetting that a screen reader or keyboard user needs the message to survive long enough to hear and reach.
| Anti-pattern | Why it fails | The fix |
|---|---|---|
| The live region and its text are created in the same step. | A screen reader only announces changes to a region that already existed, so inserting a fresh element that carries both aria-live and the message at once is usually silent (fails 4.1.3). | Ship an empty live region in the initial HTML, present on load, and set or append text into it when the event happens. |
| Every toast uses role="alert" or aria-live="assertive". | Assertive interrupts whatever the screen reader is saying, so a routine "Saved" cuts the user off mid-sentence and the interface feels hostile (weakens 4.1.3 even though it technically announces). | Default to role="status" (polite) for confirmations and counts, and reserve assertive for genuine errors and time-critical warnings. |
| A toast with an Undo button auto-dismisses after a few seconds. | A keyboard or screen reader user often cannot read the message and reach the action before it disappears, so the control is effectively unavailable (fails 2.2.1 and 2.1.1). | Never auto-dismiss a notification that carries an action or essential text; keep it until the user dismisses it, and pause any timer on hover and focus. |
| The toast moves focus to itself so the screen reader will notice it. | It pulls the user out of their current task and strands focus in a box that is about to vanish, leaving them lost (fails 2.4.3 and works against 4.1.3, which requires announcement without a focus change). | Announce through a live region without moving focus; if the message truly must interrupt and demand a response, build it as an alert dialog instead. |
| Success and error are signaled only by the toast's color. | A green or red background carries no meaning for color-blind users or for a screen reader, so the nature of the message is lost (fails 1.4.1). | Put the word Success or Error, or an icon with a text label, in the toast itself, and treat color as reinforcement rather than the message. |
| A fixed-position toast covers the control the user just focused. | The keyboard user can no longer see the field or button they are operating, because the notification sits on top of it (fails 2.4.11 Focus Not Obscured). | Position the toast stack clear of interactive content, or shift it out of the way when it would overlap the element that has focus. |
| The dismiss control is a bare glyph in a div with an onClick. | A div is not keyboard operable and an unlabeled times sign announces as nothing, so a keyboard and screen reader user cannot close the toast (fails 2.1.1 and 4.1.2). | Use a real button element with an accessible name such as "Dismiss notification", and hide the decorative glyph from assistive technology. |
The Accessible Notification & Toast Checklist
- The pattern is chosen on purpose. Ask whether the message can wait and whether the user must act on it; the answers decide toast, alert banner, or alert dialog.
- The live region exists before its first message. An empty region is in the initial HTML; text is set or appended into it, never created together with it.
- Politeness matches urgency.
role="status"for confirmations and counts,role="alert"only for errors and time-critical warnings,role="log"for a running history. - Nothing steals focus. The live region announces in place; only a message that must interrupt and demand a response becomes an alert dialog.
- Auto-dismiss respects 2.2.1. Pure confirmations may auto-hide, but the timer pauses on hover and focus and a manual close is always offered.
- Actionable and essential messages persist. A toast holding an Undo, an error to fix, or a warning to answer is never on a timer.
- The dismiss control is a real named button. It is a
<button>labelled by its action, the glyph is hidden, and Escape dismisses. - Meaning is never color alone. The toast says Success or Error in text or a labelled icon, and its text meets 4.5 to 1 against its own background.
- The stack does not flood or obscure. Additions queue in one polite region, the visible count is capped, and a fixed toast stays clear of the focused control and reflows at 320 pixels.
- Repeat messages re-announce. An identical confirmation is spoken again by clearing the region and setting the text on the next frame.
Notifications Are a Live-Region Problem
The moment a notification has to interrupt and take focus, it is a dialog. Read the criterion that governs announcing change without moving focus, and see how a focus-managed dialog is built.
Frequently Asked Questions
Should a notification use role="status" or role="alert"?▾
It depends on whether the message can wait. role="status" is an implicit polite live region: the screen reader finishes what it is saying and then reads the new text, which is right for confirmations, result counts, and progress. role="alert" is an implicit assertive live region: it interrupts whatever the screen reader is saying, which is jarring and should be reserved for genuine errors and time-critical warnings such as a session about to expire. The mistake to avoid is making every toast assertive, because a screen reader user is then interrupted on every save and every small update. Default to polite, promote to assertive only when the message truly cannot wait, and use role="log" when you are maintaining a running history like a chat transcript or an activity feed.
Why is my toast not being announced by the screen reader?▾
The most common cause is that the live region and its text are created at the same moment. A screen reader only announces changes to a region that already existed in the accessibility tree, so if your code inserts a brand new element that carries both aria-live and the message at once, the region is often not observed in time and the message is silent. The fix is to ship an empty live region in the initial HTML, present on page load, and then set or append text into it when something happens. Also confirm the region is not hidden with display:none or visibility:hidden, which removes it from the accessibility tree entirely; use a visually hidden or sr-only class that keeps the element present. If repeated identical messages fail to re-announce, that is expected: a live region does not re-read text that has not changed, so clear it first and then set it again.
Can a toast auto-dismiss and still be accessible?▾
A pure confirmation such as "Draft saved" can auto-dismiss, because the live region has already spoken the message and nothing is lost when the visual disappears. Even then, pause the dismiss timer while the pointer is hovering the toast and while keyboard focus is inside it, and always provide a manual close, so a returning user has time to read it. What must never auto-dismiss is a notification that carries an action or essential information: an Undo button, an error the user has to fix, or a warning they need to act on. Removing that on a timer is a time limit on content and collides with WCAG 2.2.1 Timing Adjustable, and it is unfair to keyboard and screen reader users who cannot always read and reach the control before it vanishes. If the user has to do something about it, do not put it on a timer.
How long should a toast stay on screen?▾
There is no single correct number, and treating it as a fixed constant is how toasts become inaccessible. A message has to stay long enough to be read and, if it has a control, long enough to be reached with a keyboard, which depends on the length of the text and the person reading it. Common design systems suggest a range of a few seconds up to around ten, but those are starting points, not rules. The honest answer is that anything the user must act on should not be on a timer at all, and everything else should pause on hover and on keyboard focus and offer a manual dismiss. If you find yourself trying to pick the perfect duration for a toast that contains an action, that is the signal the content belongs in a persistent region rather than a disappearing one.
Should a toast take keyboard focus so the screen reader notices it?▾
No. Moving focus to a toast is the wrong fix and it creates two new problems. It rips the user out of whatever they were doing, losing their place in a form or a list, and it strands focus inside a box that is often about to disappear on a timer, which can leave the user nowhere. The entire reason a live region exists is to announce a change without moving focus, and WCAG 4.1.3 Status Messages specifically requires that the change be presented without receiving focus. Keep focus where the user put it and let the polite or assertive region speak. The only time a notification legitimately takes focus is when it must interrupt the task and demand a response, and at that point it is not a toast at all: it is an alert dialog, built with the dialog pattern and its own focus management.
What is the difference between a toast, an alert banner, and an alert dialog?▾
They sit on a scale of how much they demand from the user. A toast is transient and non-modal: it appears in a corner, announces itself through a polite live region, and disappears without ever taking focus, so it suits confirmations and low-stakes information. An alert banner is persistent and in the flow of the page: it stays until dismissed or resolved and is a good home for an error that must be read and fixed, often marked up with role="alert" when it appears in response to a problem. An alert dialog is modal: it interrupts the task, takes focus, and blocks interaction with the rest of the page until the user responds, which is right for a destructive confirmation or a message that genuinely cannot be ignored. Choosing the right one comes down to whether the message can wait and whether the user has to act on it before continuing.
Do accessible toast libraries like react-hot-toast, Sonner, or Radix Toast handle all of this?▾
A good library gets the hardest part right, which is the live region: it keeps a persistent region mounted and announces new toasts through it, so you avoid the create-the-region-and-the-text-together bug. But a library cannot make the design decisions for you. You still owe the politeness choice between polite confirmations and assertive errors, a pause on hover and on keyboard focus, a real named dismiss button, the rule that a toast carrying an action does not auto-dismiss, and status that does not depend on color alone. You also still owe testing: turn a screen reader on, trigger the toast, and listen. Treat the library as a correct plumbing layer, and audit the behavior you build on top of it exactly as you would a hand-rolled component.
How do I announce the same message twice, like "Copied" clicked repeatedly?▾
A live region only announces content that has changed, so setting a region to the same text it already holds does nothing, and a user who clicks Copy twice hears the confirmation only once. The reliable trick is to clear the region and then set the text again as a separate change, giving the browser a moment in between. Set the region's text to an empty string, then on the next animation frame or after a very short timeout set it to the message. The screen reader now sees an empty region followed by a region with text, which reads as a change and is announced. This pattern is worth wrapping in a small announce helper so every repeated confirmation in your app re-announces correctly.
Essential Accessibility Resources
Comprehensive tools, checklists, and guides to help you create inclusive digital experiences