npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

sonner-a11y

v1.1.0

Published

An accessible toast component for pure JavaScript: screen-reader announcements, full keyboard operation, WCAG 2.2 AA. Fork of sonner-js.

Downloads

298

Readme

CI npm version npm downloads License: MIT


✨ Features

  • 🚀 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

🚀 Quick Start

Installation

npm install sonner-a11y

Basic Usage

import toast from "sonner-a11y";

// Simple toast
toast("Hello World!");

// Toast with description
toast("Operation successful", {
  description: "Your data has been saved",
});

📖 Usage Guide

Different Toast Types

// 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 with Action Buttons

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,
  },
});

Promise Handling

const fetchData = () => fetch("/api/data");

toast.promise(fetchData, {
  loading: "Loading...",
  success: "Data loaded successfully",
  error: "Failed to load data",
});

Update and Dismiss Toasts

// 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();

Permanent Toasts

toast("Important notice", {
  duration: 0, // Set to 0 for permanent display
});

🌐 CDN Usage

<script type="module">
  import toast from "https://cdn.jsdelivr.net/npm/sonner-a11y/+esm";

  toast("Hello from ESM!");
</script>

⚙️ Configuration Options

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. Use toast.resetConfig() to go back to the shipped defaults.

🎨 Theming

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. Without toastOptions: { 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" and theme: "dark" ship different defaults for the same role, but a token you declare wins in both — and on inverted 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-width stops applying below 600px, where the toast is laid out full-width minus mobileOffset instead.

♿ Accessibility

Toasts are announced to screen readers, fully operable from the keyboard, and honour the user's motion and contrast preferences.

Keyboard

| 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.

Screen reader announcements

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 }); // interrupt

The severity is also carried as text for screen readers, since an icon and a colour alone are not perceivable (“Error. Payment failed. Card declined.”).

Translating the labels

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" });

Other a11y options

| 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) |

Differences from Sonner (React)

  • Toasts carry no aria-live or role="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.error interrupts by default; Sonner only looks at important.
  • 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.

Notes

  • The toast title is inserted as plain text. Pass titleAsHtml: true to 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 offset or use a top-* position.

🤝 Contributing

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.

🚀 Releasing

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 pending

The 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.

📄 License

MIT License - see LICENSE file for details.