Atlas survey

Accessible toasts in React (2026): what's actually required, and which libraries deliver

No React toast library is accessible out of the box: the correct pattern is a pre-mounted live region plus a landmark hotkey, and only Base UI, React Aria and Radix implement it — Sonner is polite-only, react-hot-toast is unreachable by keyboard, notistack fires role=alert into a node that mounts with its own text.

32 sources ~12 min read #3 accessibility · react · toast · wcag · aria · screen-readers · sonner · radix

TL;DR — An accessible toast needs five things that almost no library gives you by default: a live region that is already in the DOM before the message arrives [7], role="status" (polite) for everything that isn’t an error [5], a keyboard route to the toast (F6/F8/Alt+T landmark hotkey) [29], a timer that pauses on focus, not just hover, and either no auto-dismiss or a duration the user can change [1]. Of the mainstream React libraries, Base UI Toast and React Aria Toast get the model right (non-modal alertdialog + landmark + priority split); Radix ⭐ 19.1k is close but has an open “never announced” bug [14]; Sonner ⭐ 12.6k is polite-only (your toast.error() is never assertive) and defaults to 4 s [18]; react-hot-toast ⭐ 11k is the worst of the group — no persistent live region, no hotkey, toasts not focusable [26]; notistack ⭐ 4.1k slams role="alert" on a node that mounts already populated [28] — the one thing MDN explicitly says does not work.

The three rules everything else follows from

1. The live region must pre-exist the message. This is the single most-violated rule in the ecosystem. MDN is blunt: “Do not try to dynamically add/generate an element with role="alert" that is already populated with the alert message you want announced — this generally does not lead to an announcement, as it is not a content change.” [7] The ARIA APG’s own alert example works exactly this way: the alert element is rendered empty at page load and the message is inserted into it on trigger [6]. Sara Soueidan states the rule generally: “place the live region container in the DOM as early as possible and then populate it… This ensures that the live region is monitored for updates before they happen.” [8]

A React toast library that renders <li role="status">{message}</li> when toast() is called violates this by construction. Sonner shipped exactly that bug until issue #306 — “ARIA live regions should always be present on screen so that screen readers can subscribe to their changes” — which reported silent toasts in Windows Narrator and older iOS VoiceOver [17].

2. status is the default; alert is the exception. role="alert" implies aria-live="assertive" + aria-atomic="true"; role="status" implies polite + atomic [8]. Assertive interrupts the screen reader mid-sentence and, per spec, assistive tech may discard everything already queued — so a chatty assertive toast can silently delete a polite message the user needed [9]. MDN: use alert only for urgent, time-sensitive information (session expiring, connection lost); otherwise use status [7]. Scott O’Hara’s position is that a toast is a status message and belongs in role="status" (or role="log" if kept) [10]. Radix went further and hard-codes role="status" for all toasts, with the reason in a source comment: “Toasts are always role=status to avoid stuttering issues with role=alert in SRs” — modulating urgency via aria-live instead [15].

3. Live regions cannot carry interactive content. alert, status and log all map to live regions, so a screen reader “will announce the raw text within, without any of the structure” — an Undo button inside a toast is announced as the word “Undo”, with no path to reach it [11]. Adrian Roselli’s conclusion: if it has an interactive child, it should not be a live region and should not be a toast — move focus to it and make it persistent [11]. Scott O’Hara allows a toast action only if “the contained action is also readily available outside of the toast, in the primary document” [10]. This is why Base UI and React Aria render each toast as a non-modal dialog / alertdialog and put a separate hidden live region alongside it for the announcement [31] [29] — the dialog holds the buttons, the live region holds the text.

Auto-dismiss vs WCAG 2.2

SC Level Text Toast implication
2.2.1 Timing Adjustable [1] A User can turn off / adjust ×10 / extend with 20 s warning An auto-dismissing toast is a content-set time limit → fails, unless the information is available elsewhere
2.2.3 No Timing [3] AAA “Timing is not an essential part of the event or activity” APG: “Avoid designing alerts that disappear automatically. An alert that disappears too quickly can lead to failure to meet… 2.2.3” [5]
4.1.3 Status Messages [4] AA Status must be programmatically determinable without receiving focus This is what forces the live region — and forbids focus-stealing toasts
2.4.11 Focus Not Obscured [12] AA (new in 2.2) Focused control must not be entirely hidden by author content A bottom-right toast stack over a focused button/field is a straight AA failure — rarely tested

The escape hatch, and it’s real. SC 2.2.1’s Understanding doc says “Content that operates on a timer does not need to be time adjustable if there is an alternative that does not rely on a timer” [1]. So a toast whose information is also reachable — an inline status, a notification centre, the updated row in the table — is compliant. A toast that is the only carrier of the information (the classic “Undo” or the only error report) is not. Whether SC 2.2.1 formally bites toasts at all has been an open W3C question since 2019: Patrick Lauke’s w3c/wcag#976 ⭐ 1.5k asks “does a disappearing toast represent a timeout?” and is still open with no working-group resolution [2]. Practitioner audits do not wait for it: an April 2026 shadcn/ui audit (WCAG 2.2 AA; VoiceOver, NVDA, macOS Safari / Windows Firefox+Chrome) flags Sonner’s 4-second default as a 2.2.1 failure — “For users with cognitive disabilities or slow readers, this is too fast” — and recommends 8 s minimum plus a close button [20]. React Aria’s docs independently recommend 5 s minimum and “Only auto-dismiss toasts when the information is not critical, or may be found elsewhere” [29].

Roselli’s fuller failure list for typical toasts: 1.3.2, 1.4.4, 1.4.10, 2.1.1, 2.2.1, 2.4.3, 3.2.4, 4.1.2, 4.1.3 [11].

Keyboard: the toast you can see but cannot reach

Toasts must not steal focus — APG’s keyboard interaction for alert is literally “Not applicable”, because “it is crucial they do not affect keyboard focus” [5]. But a toast rendered into a portal at the end of <body> is also after everything in tab order, and it may vanish before the user tabs to it. The industry answer is a landmark + hotkey:

Library Hotkey Mechanism
React Aria F6 / Shift+F6 Toast region is a landmark; LandmarkManager cycles landmarks. On close, “focus moves to the next toast… when the last toast is closed, focus is restored to where it was before” [29]
Base UI ⭐ 10.3k F6 Global keydown force-focuses the viewport landmark [31]
Radix ⭐ 19.1k F8 VIEWPORT_DEFAULT_HOTKEY = ['F8'], viewport is role="region" with an aria-label that includes the hotkey [15]; two sentinel focus-proxy spans reverse tab order so the newest toast comes first [13]
react-toastify ⭐ 13.4k Alt+T (v11, Dec 2024) hotKeys: e => e.altKey && e.code === 'KeyT' focuses the first [tabIndex="0"] toast [24] [23]
Sonner ⭐ 12.6k Alt+T hotkey = ['altKey','KeyT']; container is tabIndex={-1} (“remove from normal navigation flow, only available via hotkey”), each toast tabIndex={0} [18]
react-hot-toast ⭐ 11k none Toaster renders plain <div>s; no hotkey, no tabIndex, no landmark [26]
notistack ⭐ 4.1k none No landmark, no hotkey [28]

⚠ Announcing the hotkey matters as much as having one: Radix and Sonner both interpolate the hotkey into the region’s accessible name (“Notifications F8” / “Notifications alt+T”) so a screen-reader user learns it exists [15] [18]. A hotkey nobody is told about is not an affordance.

Screen readers in practice

  • The mount-time swallow. NVDA in particular does not announce a role="status" element that appears already containing text. Radix works around it by rendering the announce node’s text on the next animation frame — the source comment is explicit: “render text content in the next frame to ensure toast is announced in NVDA” [15]. Sandro Roth’s August 2025 teardown documents the same: “If an element with role="status" is added to the page, its content will not be announced by NVDA” — Radix therefore renders it empty first, then re-renders with content after a delay [13].
  • Double announcement. Composing one message from several DOM insertions makes screen readers “make multiple separate message announcements”; the practical fix when reusing a live region is to blank it and re-fill after ~350–500 ms [9]. Radix’s announce portal keeps the announce text mounted for 1000 ms before removing it [15].
  • Dropped announcements. react-toastify’s role="alert" toasts are not announced at all in Firefox+Orca (they are in Chrome+Orca); the issue quotes MDN’s “do not dynamically add a populated role=alert” rule and remains open [22]. Radix’s radix-ui/primitives#3634 (July 2025, v1.2.14, open) reports aria-live="off" landing on the role="status" element in the official docs demo, reproduced across Chrome, Safari and Firefox — “its presence is not announced by screen readers” [14]. (The current main source sets aria-live={type === 'foreground' ? 'assertive' : 'polite'}, so if you’re on Radix, pin-check your version [15].)
  • Virtual cursor ≠ focus. Screen-reader users reading a toast with the virtual cursor generate no focus event, so any “pause while focused” logic never fires for them — they are racing the timer like everyone else [10].
  • aria-live is advisory, not binding. “ARIA live region properties are only strong suggestions” — browsers and AT may override them. Test the combination you ship, don’t trust the attribute [9].

Pause behaviour and reduced motion

Pause-on-hover is a mouse feature. It does nothing for keyboard or screen-reader users. The accessible version is pause on hover + focus + window blur + document hidden.

Library Pause on hover Pause on focus Pause on blur / hidden prefers-reduced-motion
Radix ⭐ 19.1k [16] via your own CSS (unstyled)
React Aria [29] [13] via your own CSS (unstyled)
Base UI ⭐ 10.3k [30] [30] via your own CSS (unstyled)
Sonner ⭐ 12.6k ✓ (isDocumentHiddenpauseTimer()) [18] @media (prefers-reduced-motion) { … transition: none !important; animation: none !important; } [19]
react-toastify ⭐ 13.4k pauseOnHover — (pauseOnFocusLoss = window blur, not element focus) [21] ✗ no reduced-motion query in shipped CSS [24]
react-hot-toast ⭐ 11k ✓ only onMouseEnter/onMouseLeave on the container [26]
notistack ⭐ 4.1k ✓ (MUI Snackbar onMouseEnter) [28]

Sonner is the only one of the batch that ships its own reduced-motion guard — a notable inversion, since it is also the one with the most motion.

Library scorecard

Library Live region pre-mounted Polite/assertive split Keyboard route Default duration Verdict
Base UI Toast ⭐ 10.3k ✓ viewport aria-live="polite" aria-atomic="false" + separate role="alert" region for priority="high" [31] priority: 'low' \| 'high' [30] ✓ F6 5000 ms; 0 = never [30] Best model. Toast root is a non-modal dialog/alertdialog [32] so actions are reachable; announcement lives in a separate region. Raise the duration.
React Aria Toast ⭐ 15.7k (react-spectrum) alert inside a non-modal alertdialog [13] ✓ F6 / Shift+F6, focus restored on last close [29] none by default; docs push ≥5 s [29] Best documented. Only library whose docs tell you not to auto-dismiss critical info.
Radix Toast ⭐ 19.1k ✓ portal’d announce node, text rendered next frame for NVDA [15] type: 'foreground' \| 'background' → assertive/polite [16] ✓ F8 5000 ms ⚠ Right design, but #3634 (open) says the docs demo emits aria-live="off" and announces nothing [14]. Verify with a real SR before shipping.
Sonner ⭐ 12.6k ✓ persistent <section aria-live="polite" aria-relevant="additions text" aria-atomic="false"> (fixed after #306) [18] [17] polite only — no assertive/alert anywhere in the source; toast.error() waits its turn behind whatever the SR is reading [18] ✓ Alt+T 4000 ms — flagged as a 2.2.1 failure by the Apr 2026 audit [20] ⚠ Good bones (reduced-motion, full pause matrix, hotkey in the label). Set duration={8000} + closeButton, and don’t rely on it for urgent errors.
react-toastify ⭐ 13.4k ✓ container carries aria-live="polite" aria-relevant="additions text" since v11 [24] ⚠ inverted: every toast defaults to role="alert" [21] — success messages interrupt ✓ Alt+T (v11) [23] 5000 ms ⚠ v11 (Dec 2024) closed the biggest gaps, but #1122 (open) reports role="alert" toasts silent in Firefox+Orca [22]. Set role="status" globally.
react-hot-toast ⭐ 11k ariaProps: { role: 'status', 'aria-live': 'polite' } are spread onto the message div that mounts with its text [26] — the MDN anti-pattern [7] ✗ (#336, open since Jan 2024, asks for a coherent ariaRole API and notes the role=alert+aria-live=assertive double-up) [25] ✗ none — toasts are not focusable 4000 ms (error 4000, loading ∞) [27] Avoid for anything a11y-sensitive. No landmark, no focus route, no focus-pause, no reduced-motion. Its Undo buttons are decorative for keyboard users.
notistack ⭐ 4.1k role="alert" set directly on MaterialDesignContent, which mounts already populated [28] alert for all variants including success/info ✗ none 5000 ms (MUI Snackbar) ✗ Worst ARIA of the group: assertive-everything, and the one pattern MDN says produces no announcement at all.

The uncomfortable conclusion

Roselli’s position — that a rich, actionable toast can’t be made accessible and should be a persistent, focusable, non-modal region instead — is no longer fringe. GitHub removed toasts from Primer’s design system in late 2025 “because of their accessibility and usability issues” [11]. The defensible 2026 pattern is:

  1. Toast = text only, role="status", injected into a pre-mounted region. No buttons.
  2. Any action offered in the toast is also available in the page (the undone row still has an Undo control).
  3. Errors that block the user are not toasts — they’re inline, focus-managed messages.
  4. Auto-dismiss ≥ 8 s, pausable on hover and focus, with a close button; or no auto-dismiss at all when the toast is the sole carrier of the information [1] [20].
  5. Provide the landmark hotkey and name it in the region label [15].

If your toast has an Undo button and a 4-second timer, no library choice will save you.

Citations · 32 sources

Click the Citations tab to load…