An accessible toast component designed for Pure JavaScript
An accessibility-focused fork of sonner-js by huanfei, itself built on Sonner
- ๐ Zero Dependencies - Pure JavaScript implementation, no frameworks required
- ๐ฑ Responsive Design - Perfect adaptation for mobile and desktop
- ๐จ Multiple Styles - Support for success, error, warning, info and more types
- โก Lightweight - Small bundle size with excellent performance
- ๐ง Highly Customizable - Rich configuration options
- ๐ Theme Support - Built-in light and dark themes
- ๐ฆ ESM - tree-shakeable (
sideEffects: false), consumed by every modern bundler - โฟ Accessible - Screen reader announcements, full keyboard operation, WCAG 2.2 AA
npm install sonner-a11yimport toast from "sonner-a11y";
// Simple toast
toast("Hello World!");
// Toast with description
toast("Operation successful", {
description: "Your data has been saved",
});// Success toast
toast.success("Operation successful");
// Error toast
toast.error("Operation failed");
// Info toast
toast.info("This is an information");
// Warning toast
toast.warning("Please note");toast("Confirm action", {
action: {
label: "Confirm",
onClick: () => console.log("User clicked confirm"),
},
});
// With cancel button
toast("Confirm deletion", {
action: {
label: "Cancel",
onClick: () => console.log("User cancelled operation"),
cancel: true,
},
});const fetchData = () => fetch("/api/data");
toast.promise(fetchData, {
loading: "Loading...",
success: "Data loaded successfully",
error: "Failed to load data",
});// Create toast and get ID
const toastId = toast("Processing...");
// Update toast
toast.success("Processing complete", { id: toastId });
// Dismiss specific toast
toast.dismiss(toastId);
// Dismiss all toasts
toast.dismiss();toast("Important notice", {
duration: 0, // Set to 0 for permanent display
});<script type="module">
import toast from "https://cdn.jsdelivr.net/npm/sonner-a11y/+esm";
toast("Hello from ESM!");
</script>import toast from "sonner-a11y";
// Global configuration
toast.config({
theme: "dark", // 'light' | 'dark'
expand: true, // Expand animation
visibleToasts: 3, // Number of visible toasts
gap: 8, // Toast spacing
offset: 16, // Margin
mobileOffset: 16, // Mobile margin
dir: "ltr", // Text direction
toastOptions: {
position: "top-right", // Position
duration: 4000, // Duration in milliseconds
closeButton: true, // Show close button
richColors: true, // Rich colors
invert: false, // Invert the colours of the toast
important: "auto", // Screen-reader politeness, see Accessibility
titleAsHtml: false, // Interpret `title` as HTML
},
});
toast.config()merges into the current configuration, so you can call it more than once and only pass what changes. Usetoast.resetConfig()to go back to the shipped defaults.
Toasts render inside a shadow root, so your stylesheets cannot reach them: a class name you passed in
would be styled by rules that live in the document, and document rules never cross the shadow
boundary. Inherited custom properties do. Theming is therefore a set of tokens you declare on the
host element โ or on any ancestor of it, :root included โ which the library reads from inside.
/* Your own stylesheet. `[data-sonner-toasters]` is the host the library appends to <body>. */
[data-sonner-toasters] {
--sonner-success-bg: #0f2e1d;
--sonner-success-text: #7ee2a8;
--sonner-border-radius: 4px;
}Every token falls back to the shipped default, so declare only the ones you want to change.
| Token | Default (light / dark) | Applies to |
|---|---|---|
--sonner-normal-bg |
#fff / #000
|
Toast background, and the close button |
--sonner-normal-bg-hover |
โ / hsl(0, 0%, 12%)
|
Hovered action button |
--sonner-normal-border |
--sonner-gray4 / hsl(0, 0%, 20%)
|
Toast border |
--sonner-normal-border-hover |
โ / hsl(0, 0%, 25%)
|
Hovered action button border |
--sonner-normal-text |
--sonner-gray12 / --sonner-gray1
|
Title, close button, focus ring |
--sonner-normal-text2 |
#3f3f3f / #e8e8e8
|
Description |
--sonner-normal-cancel-bg |
rgba(0, 0, 0, 0.08) / rgba(255, 255, 255, 0.3)
|
Cancel button background |
--sonner-success-bg, -border, -text
|
greens |
success toasts, with richColors
|
--sonner-info-bg, -border, -text
|
blues |
info toasts, with richColors
|
--sonner-warning-bg, -border, -text
|
ambers |
warning toasts, with richColors
|
--sonner-error-bg, -border, -text
|
reds |
error toasts, with richColors
|
--sonner-width |
356px |
Toast width, above a 600px viewport |
--sonner-border-radius |
8px |
Toast and button corners |
--sonner-gray1 โฆ --sonner-gray12
|
neutral ramp | Loader, buttons, and the defaults above |
Three things worth knowing before reaching for them:
-
The severity colours need
richColors. WithouttoastOptions: { richColors: true }every toast uses the--sonner-normal-*set whatever its type, and the four severity groups are inert. -
One token covers both themes.
theme: "light"andtheme: "dark"ship different defaults for the same role, but a token you declare wins in both โ and oninverted toasts too. So give it a value that is already theme-aware on your side, such as a design-system variable that changes under your own dark-mode selector. -
--sonner-widthstops applying below 600px, where the toast is laid out full-width minusmobileOffsetinstead.
Toasts are announced to screen readers, fully operable from the keyboard, and honour the user's motion and contrast preferences.
| Key | Effect |
|---|---|
| Alt+T | Move focus to the most recent toast and expand the stack |
| Tab / Shift+Tab | Walk through the toasts and their buttons |
| โ โ / โ โ | Next / previous toast |
| Home / End | First / last toast |
| Delete / Backspace | Dismiss the focused toast (the keyboard equivalent of swiping) |
| Esc | Collapse the stack and return focus to where it was |
Auto-dismiss timers pause while the pointer is over the toasts, while focus is inside them, and
while the tab is hidden. Use duration: 0 for a toast that never closes on its own.
Announcements go through a dedicated live region kept in the light DOM, outside the shadow root.
Errors interrupt (aria-live="assertive"), everything else is announced politely. Override it per
toast with important:
toast.error("Payment failed", { important: false }); // announce politely
toast("Build finished", { important: true }); // interruptThe severity is also carried as text for screen readers, since an icon and a colour alone are not perceivable (โError. Payment failed. Card declined.โ).
Defaults are in English. Everything a screen reader reads can be replaced:
toast.config({
a11y: {
hotkey: ["altKey", "KeyN"], // modifier properties and/or KeyboardEvent.code values
labels: {
region: "Notifications", // `{hotkey}` is substituted, otherwise appended in parentheses
close: "Fermer la notification",
action: "Action",
types: {
success: "Succรจs",
error: "Erreur",
info: "Information",
warning: "Avertissement",
loading: "Chargement",
},
},
},
});Per toast, typeLabel overrides the severity label:
toast.error("HTTP 502", { typeLabel: "Erreur serveur" });| Option | Default | Effect |
|---|---|---|
announce |
true |
Announce toasts through the live region |
announceClearDelay |
1000 |
How long the announced text stays in the region, in ms |
hotkey |
['altKey', 'KeyT'] |
Key combination that focuses the most recent toast |
pauseOnHover |
true |
Pause the timers while the pointer is over the toasts |
pauseOnFocus |
true |
Pause the timers while focus is inside a toast |
pauseOnDocumentHidden |
true |
Pause the timers while the tab is hidden |
dismissOnEscape |
false |
Make Esc dismiss the focused toast instead of leaving |
dismissKeys |
['Delete', 'Backspace'] |
Keys that dismiss the focused toast ([] disables it) |
- Toasts carry no
aria-liveorrole="status": since this port renders into a shadow root whose container is created and destroyed on demand, a live region there is not reliably announced. A single dedicated region in the light DOM is used instead, which also rules out double announcements. -
toast.errorinterrupts by default; Sonner only looks atimportant. - The hotkey focuses the most recent toast rather than the list, so the message is read straight away.
- Esc also restores focus to the element that had it before.
- The toast title is inserted as plain text. Pass
titleAsHtml: trueto opt back into HTML โ the caller is then responsible for sanitising it. - A fixed-position toast can cover the element that currently has focus (WCAG 2.4.11). If that
matters for your layout, raise
offsetor use atop-*position.
Bug reports, accessibility reports and pull requests are welcome. Start with
CONTRIBUTING.md: it covers the test bench served by pnpm dev, how to verify a
change with a screen reader and a keyboard, and the invariants in the code that are load-bearing.
By participating you agree to the Code of Conduct. To report a vulnerability, see SECURITY.md.
Version numbers are managed by Changesets โ never edit the
version field by hand.
Every pull request that changes the published package must ship a changeset describing its intent:
pnpm changeset # pick patch / minor / major, write the summary
pnpm changeset:status # what is pendingThe summary lands verbatim in CHANGELOG.md, so write it for the consumer: what they can now do, in
the present tense. For a docs- or CI-only pull request, add #skip-changeset to the title instead.
Once merged into main, a chore: version packages pull request is opened (or updated) with the
version bump and the changelog entry. Merging it releases nothing on its own.
CI never publishes. Publishing runs from a real machine, so the tarball that reaches npmjs is the one that was verified locally, and no long-lived npm token has to live in CI:
pnpm release:dry # run every check, publish nothing
pnpm release # publish to npmjs, then tag <name>@<version>The script refuses to publish a dirty tree, a branch other than main, a main out of sync with the
remote, or a version whose sources changed after Changesets set it โ otherwise npm, the changelog and
git would describe different trees. A version already on npm is a no-op, not an error, and the tag is
only pushed once the publish succeeded.
MIT License - see LICENSE file for details.