mrkeyoor.com_
Thu 06 Aug 02:41 UTC
npmWeb Frontendupdated 05 Aug 2026

sonner

Sonner is a toast component for React. You render a single <Toaster /> somewhere near the root of your app, then call toast('Saved') from anywhere, including outside React components, and a notification stacks into the corner. It ships the parts people usually rebuild by hand: success, error, warning, info and loading variants, a promise helper that swaps the message when your async call settles, action and cancel buttons, swipe to dismiss, keyboard focus handling, pause on hover, a light and dark theme, and stacking with expand on hover. Styling comes from CSS variables and data attributes, and every element can be given your own class names.

Verdict

The best default for React toasts right now: the defaults are the ones you would have picked, and the promise helper alone saves real code. The honest caveat is release cadence, so pin the version and read the repo before you assume a bug is fixed.

API stability4/5The toast() and <Toaster /> surface has barely moved since 1.0, and the 2.0 major was mostly about swipe directions, offsets, and React 19 support rather than renamed APIs.
Docs4/5sonner.emilkowal.ski is a live playground where every prop is demonstrated and copyable, which beats most component docs; the README is four paragraphs, and edge cases like CSP and multiple Toasters are not written down anywhere.
Maintenance3/5The repo was pushed in August 2026 and the tracker is small (46 open issues, 37 open PRs), but the last npm publish was 2.0.7 in August 2025, so merged fixes are not reaching users.
Ecosystem5/5Roughly 49M downloads a week, shipped as the toast component in shadcn/ui, and ported to other frameworks by the community as vue-sonner and svelte-sonner.

Use it if

  • You want good-looking toasts in a React app in two lines of code, with sensible animation, stacking, and hover-to-expand behavior already decided for you
  • You show a lot of async feedback and want toast.promise to handle the loading, resolved, and rejected states of a single fetch without three separate calls
  • You care about the details most toast libraries get wrong: swipe dismissal, focus not being stolen, timers pausing when the window is hidden, and an accessible live region for screen readers
  • You are on Tailwind or a design system and want to restyle every part through classNames or drop the built-in CSS entirely with unstyled
Skip it if

Setup reality

npm install sonner, drop <Toaster /> in your root layout, call toast(). There is no CSS import step because the styles are injected at runtime, which is convenient until a CSP or a style-order requirement gets in the way. In Next.js App Router the layout is a server component, so <Toaster /> must live in a file marked 'use client', and any component calling toast has to be a client component too; server actions have to return a value the client turns into a toast. Two Toaster instances mounted at once means every toast renders twice, which is easy to do when a shared layout and a page both add one; if you genuinely need two, give each an id and pass toasterId on the toast. Dark mode is not automatic, theme defaults to light, so with next-themes you pass theme={resolvedTheme} yourself.

Patterns

Mount the Toaster and show a toastmount-and-fire

'use client';
import { Toaster, toast } from 'sonner';

export default function App() {
  return (
    <>
      <Toaster position="bottom-right" richColors closeButton />
      <button onClick={() => toast('Settings saved')}>Save</button>
    </>
  );
}

Exactly one <Toaster /> per app. Mounting a second one, for example in both a layout and a page, renders every toast twice with no error to tell you why.

Use the built-in status variantstoast-variants

import { toast } from 'sonner';

toast.success('Profile updated');
toast.error('Could not reach the server');
toast.warning('Your trial ends tomorrow');
toast.info('A new version is available');
toast.message('Plain toast with a description', {
  description: 'Monday, January 3rd at 6:00pm',
});

Variants only get colored backgrounds when richColors is set on the Toaster; without it they are the neutral style with a colored icon.

Track an async call with one toasttoast-promise

import { toast } from 'sonner';

toast.promise(saveUser(form), {
  loading: 'Saving...',
  success: (user) => `${user.name} saved`,
  error: (err) => `Save failed: ${err.message}`,
  finally: () => setSubmitting(false),
});

toast.promise does not rethrow, so the rejection is considered handled. If you need the value or the error in your own code, call .unwrap() on the return value and await it inside a try/catch.

Add an undo actionaction-and-cancel-buttons

import { toast } from 'sonner';

toast('Message archived', {
  action: {
    label: 'Undo',
    onClick: () => restoreMessage(id),
  },
  cancel: {
    label: 'Dismiss',
    onClick: () => {},
  },
  duration: 10000,
});

Clicking the action dismisses the toast as well as running onClick. Give undo toasts a longer duration than the default 4 seconds, otherwise the button is gone before anyone reads it.

Update a toast in placeupdate-existing-toast

import { toast } from 'sonner';

const id = toast.loading('Uploading...');

await upload(file);

toast.success('Upload complete', { id });

Passing the same id replaces the content and type without a new entrance animation. This is the manual version of toast.promise, and the only way to drive a toast from progress events.

Dismiss one toast or all of themdismiss-toasts

import { toast } from 'sonner';

const id = toast('Connecting...', { duration: Infinity });

toast.dismiss(id);   // just this one
toast.dismiss();     // every visible toast

toast('Never auto-closes', {
  duration: Infinity,
  onDismiss: (t) => console.log('closed', t.id),
  onAutoClose: (t) => console.log('timed out', t.id),
});

duration: Infinity means the user must close it, so pair it with closeButton or an action. onDismiss fires for manual closes, onAutoClose for the timer; they are not interchangeable.

Render your own component as a toastcustom-toast-jsx

import { toast } from 'sonner';

toast.custom((id) => (
  <div className="rounded-lg border bg-white p-4 shadow">
    <p className="font-medium">Deploy finished</p>
    <button onClick={() => toast.dismiss(id)}>Close</button>
  </div>
), { duration: 8000 });

toast.custom hands you the id and expects you to build everything, including the close affordance. You keep positioning, stacking, and swipe behavior; you lose the default styling, icons, and buttons.

Restyle toasts with your own classesstyle-with-classnames

import { Toaster } from 'sonner';

<Toaster
  toastOptions={{
    unstyled: true,
    classNames: {
      toast: 'flex w-full gap-2 rounded-lg border bg-white p-4 shadow-lg',
      title: 'text-sm font-medium',
      description: 'text-sm text-gray-500',
      actionButton: 'rounded bg-black px-2 py-1 text-xs text-white',
      error: 'border-red-300 bg-red-50',
    },
  }}
/>

unstyled removes the built-in look but keeps layout and animation. Without it your classes fight the injected CSS and you end up writing !important; with it you must style every state yourself, including success and error.

Follow the app's dark modetheme-with-next-themes

'use client';
import { useTheme } from 'next-themes';
import { Toaster } from 'sonner';

export function Toasts() {
  const { resolvedTheme } = useTheme();
  return <Toaster theme={resolvedTheme === 'dark' ? 'dark' : 'light'} />;
}

theme defaults to 'light', not 'system', so a dark app shows white toasts until you wire this up. Use resolvedTheme rather than theme so 'system' resolves to an actual value.

Control how many toasts show and how they stacktune-stacking-behavior

import { Toaster } from 'sonner';

<Toaster
  expand
  visibleToasts={5}
  gap={12}
  duration={6000}
  offset={{ top: 24, right: 24 }}
  mobileOffset={16}
  swipeDirections={['right']}
/>

Without expand, toasts collapse into a stack and only fan out on hover. visibleToasts caps what is rendered; extra toasts queue rather than being dropped, so a burst of errors takes a while to drain.

Read the current toasts from Reactread-active-toasts

import { useSonner } from 'sonner';

function UnreadBadge() {
  const { toasts } = useSonner();
  return toasts.length ? <span>{toasts.length}</span> : null;
}

useSonner reflects what is on screen right now. For everything that has been shown this session there is toast.getHistory(), which is memory only and resets on reload.

Fire a toast from an API client or storetoast-from-non-react-code

// api-client.ts, no React involved
import { toast } from 'sonner';

export async function apiFetch(url: string, init?: RequestInit) {
  const res = await fetch(url, init);
  if (!res.ok) {
    toast.error(`${res.status} ${res.statusText}`);
    throw new Error(res.statusText);
  }
  return res.json();
}

toast() writes to a module-level store, so it works outside components and hooks. It is still a no-op on the server, so guard calls in code that also runs during SSR or in a route handler.

Alternatives

PackageRegistryPick it when
react-hot-toastnpmYou want a similar hooks-friendly API with a smaller feature surface and a longer release history.
react-toastifynpmYou need heavy configurability, containers per region, and progress bars, and do not mind a larger and more verbose API.
@radix-ui/react-toastnpmYou want an unstyled accessible primitive and intend to build the visual layer and queueing yourself.