Skip to main content

react-hot-toast vs react-toastify vs Sonner: Toast Notifications in 2026

·PkgPulse Team

TL;DR

For new projects in 2026: Sonner by Emil Kowalski is the most elegant, accessible, and performant toast library — it's become the community default since shadcn/ui adopted it. react-hot-toast is a solid, minimal choice if you need the smallest bundle with a clean API. react-toastify is the legacy leader — functional but shows its age in the age of Tailwind.

Key Takeaways

  • react-toastify: ~4.5M weekly downloads — most installed, legacy-heavy, largest bundle
  • react-hot-toast: ~1.2M weekly downloads — minimal API, tiny bundle (4KB), great DX
  • Sonner: ~1.8M weekly downloads — shadcn/ui default, stacked toast design, best accessibility
  • Sonner has grown ~400% in 12 months — fastest adoption in this category
  • All three are functional for most projects; the choice is aesthetics + philosophy
  • Sonner wins for new projects: accessible, stacked design, smooth animations

PackageWeekly DownloadsBundle SizeHeadless?
react-toastify~4.5M~42KB
sonner~1.8M~11KBPartial
react-hot-toast~1.2M~4KB

Sonner

Sonner by Emil Kowalski (also creator of vaul, cmdk) is now the standard in the Shadcn/ui ecosystem:

// 1. Add Toaster component once (in layout):
import { Toaster } from "sonner"

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        {children}
        <Toaster richColors />
      </body>
    </html>
  )
}

// 2. Call toast() anywhere:
import { toast } from "sonner"

// Basic toast types:
toast("Event created")
toast.success("Package published successfully!")
toast.error("Failed to connect to npm registry")
toast.warning("Package is 2 years behind on releases")
toast.info("New version available: 4.2.0 → 4.3.0")

// Loading state with promise:
toast.promise(
  publishPackage(packageName),
  {
    loading: "Publishing…",
    success: (data) => `${data.name}@${data.version} published!`,
    error: (err) => `Error: ${err.message}`,
  }
)

// Action button in toast:
toast("New package added", {
  action: {
    label: "View",
    onClick: () => router.push(`/packages/${name}`),
  },
  cancel: {
    label: "Undo",
    onClick: () => removePackage(name),
  },
})

Sonner's stacked design:

Sonner stacks toasts in a collapsed view when multiple are present — only the topmost is fully visible. This prevents toast storms from flooding the UI.

// Customization:
<Toaster
  position="bottom-right"
  richColors          // Colored backgrounds for success/error/warning
  closeButton         // Show × close button
  theme="dark"        // dark | light | system
  duration={4000}     // Auto-dismiss after 4 seconds
  visibleToasts={3}   // Max toasts visible (default 3)
  toastOptions={{
    classNames: {
      toast: "font-sans",
      success: "text-green-600",
      error: "!bg-red-50 !border-red-200",
    },
  }}
/>

Custom Sonner toast:

// Rich custom content:
toast.custom((t) => (
  <div className="flex items-center gap-3 bg-white border rounded-lg shadow p-4">
    <PackageIcon className="text-blue-500 h-5 w-5" />
    <div>
      <p className="font-medium">react-query updated</p>
      <p className="text-sm text-gray-500">5.0.0 → 5.28.0 available</p>
    </div>
    <button onClick={() => toast.dismiss(t)}>×</button>
  </div>
))

react-hot-toast

react-hot-toast prioritizes simplicity and tiny bundle size:

import { Toaster, toast } from "react-hot-toast"

// Setup — minimal:
function App() {
  return (
    <div>
      <Toaster
        position="top-right"
        toastOptions={{
          duration: 4000,
          style: {
            background: "#363636",
            color: "#fff",
          },
          success: { duration: 3000 },
        }}
      />
    </div>
  )
}

// Usage:
toast("Package published!")
toast.success("Deployed to production")
toast.error("Build failed — check logs")
toast.loading("Deploying…")

// Promise toast — auto-transitions loading → success/error:
const deployToast = toast.loading("Starting deployment…")
try {
  const result = await deploy()
  toast.success("Deployed!", { id: deployToast })
} catch (error) {
  toast.error("Deploy failed", { id: deployToast })
}

// Using toast.promise (cleaner):
toast.promise(deploy(), {
  loading: "Deploying…",
  success: "Deployed!",
  error: "Deployment failed",
})

react-hot-toast custom rendering:

// Fully headless — render your own component:
toast.custom((t) => (
  <div
    className={`${t.visible ? "animate-enter" : "animate-leave"} max-w-md w-full bg-white shadow-lg rounded-lg pointer-events-auto flex`}
  >
    <div className="flex-1 w-0 p-4">
      <p>{t.message as string}</p>
    </div>
    <button
      onClick={() => toast.dismiss(t.id)}
      className="p-4 border-l"
    >
      Dismiss
    </button>
  </div>
))

react-hot-toast's custom rendering is the most flexible — the <Toaster> is literally just an anchor point, and you control 100% of the markup.


react-toastify

react-toastify is the legacy leader — it was the default choice for years and is still the most downloaded:

import { ToastContainer, toast } from "react-toastify"
import "react-toastify/dist/ReactToastify.css"  // Must import CSS

function App() {
  return (
    <div>
      {/* Required container */}
      <ToastContainer
        position="top-right"
        autoClose={5000}
        hideProgressBar={false}
        newestOnTop={false}
        closeOnClick
        rtl={false}
        pauseOnFocusLoss
        draggable
        pauseOnHover
        theme="light"
      />
    </div>
  )
}

// Usage:
toast("Default toast")
toast.success("Operation successful!")
toast.error("Something went wrong!")
toast.warn("This may cause issues")
toast.info("Just so you know…")

// With options:
toast.success("Saved!", {
  position: "bottom-left",
  autoClose: 3000,
  hideProgressBar: true,
  closeOnClick: true,
  pauseOnHover: true,
  draggable: true,
})

Why react-toastify's downloads are inflated:

The ~4.5M weekly downloads are largely from existing projects that installed it years ago and haven't migrated. The mandatory CSS import is a pattern from pre-Tailwind times. The progress bar and default styling look dated in modern Tailwind-based UIs.


Feature Comparison

FeatureSonnerreact-hot-toastreact-toastify
Bundle size~11KB~4KB~42KB
CSS import required✅ Required
Stacked toast design❌ Linear❌ Linear
Promise support
Custom components✅ Headless
Accessibility (ARIA)✅ Excellent✅ Good✅ Good
Position options✅ 6 positions✅ 6 positions✅ 6 positions
Drag to dismiss
Progress bar
RTL support
shadcn/ui default✅ Yes
TypeScript

Accessibility Comparison

All three use the ARIA role="status" or role="alert" pattern for screen readers. Sonner's implementation is the most polished:

// Sonner — announces toasts to screen readers properly:
<Toaster
  richColors
  // Sonner uses aria-live regions with appropriate politeness levels:
  // - success/info: aria-live="polite"
  // - error/warning: aria-live="assertive" (immediately interrupts)
/>

react-hot-toast uses a visually hidden <div aria-live="assertive"> for announcements — correct but requires verification for complex custom renders.


Sonner + shadcn/ui Integration

If you're using shadcn/ui, Sonner is included via the CLI:

npx shadcn-ui@latest add sonner

This generates a themed <Toaster> component that respects your design system:

// components/ui/sonner.tsx (auto-generated):
import { Toaster as Sonner } from "sonner"
import { useTheme } from "next-themes"

type ToasterProps = React.ComponentProps<typeof Sonner>

export function Toaster({ ...props }: ToasterProps) {
  const { theme = "system" } = useTheme()

  return (
    <Sonner
      theme={theme as ToasterProps["theme"]}
      className="toaster group"
      toastOptions={{
        classNames: {
          toast: "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground ...",
          // Inherits your shadcn CSS variables
        },
      }}
      {...props}
    />
  )
}

When to Use Each

Choose Sonner if:

  • Starting a new project in 2026
  • Using shadcn/ui (it's the default — don't override it)
  • You want stacked toasts that don't flood the UI
  • Accessibility and modern design aesthetics matter

Choose react-hot-toast if:

  • You need the smallest possible bundle (4KB)
  • You want fully headless custom renders with no default styling opinions
  • Simple promise-to-toast flow is the primary use case

Choose react-toastify if:

  • You have an existing react-toastify implementation
  • You want the progress bar UX
  • You need drag-to-dismiss

Methodology

Download data from npm registry (weekly average, February 2026). Bundle sizes from bundlephobia. Accessibility notes from WCAG audits and library documentation.

Compare notification library packages on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.