Decision. From
toastr: migrate now. Last commit June 2018 ⭐ 12k (Jul 2026) [1], last npm publish 2.1.4 in Dec 2017, hardjquery >=1.12.0dependency [2] — it imperatively appends DOM outside React and breaks SSR/concurrent rendering. Fromreact-toastify: don’t. v11.1.0 shipped Apr 2026, actively maintained ⭐ 13k (Jul 2026) [3]. A working integration is not a migration trigger. Move only if you’re adopting shadcn/ui (Sonner is its default toast) or you want stacking/gestures for free — Sonner ⭐ 13k (Jul 2026) [4], ~189M npm downloads/month vs react-toastify’s ~16M [5].
API mapping
| Legacy | Sonner |
|---|---|
toastr.success(msg) / toastr.error(msg, title) |
toast.success(msg) / toast.error(title, { description: msg }) [6] |
toastr.options = {...} (global mutable) |
<Toaster toastOptions={{...}} /> props [7] |
toast.success(msg, opts) (toastify) |
toast.success(msg, opts) — near 1:1 [6] |
toast.loading() → toast.update(id, {render, type}) |
const id = toast.loading(m); toast.success(m2, { id }) [6] |
toast.promise(p, {pending, success, error}) |
toast.promise(p, {loading, success, error}) — pending→loading; callbacks get the resolved value [6] |
toast.dismiss(id), toast.isActive(id) |
toast.dismiss(id); no isActive — track ids yourself |
<ToastContainer rtl /> |
<Toaster dir="rtl" /> (ltr/rtl/auto) [7] |
containerId multi-region |
<Toaster id="x" /> + toast(m, { toasterId: 'x' }) [7] |
No equivalent — rebuild or drop
| react-toastify feature | Sonner reality |
|---|---|
Progress bar (hideProgressBar, progress for manual control) [8] |
✗ none. Rebuild as custom JSX + CSS animation, or drop. |
transition / cssTransition({enter, exit, collapse}) [9] |
✗ no transition API. Animations are baked in; you get data-* attribute CSS hooks + classNames only. |
toast.update(id, { render, type, autoClose, className }) [10] |
Partial: re-calling toast.<type>(msg, { id }) replaces content/type. No per-field patching, no update-transition. |
closeButton={MyButton} custom component |
Only closeButton bool + classNames.closeButton / icons. Custom → toast.custom(). |
draggable, pauseOnFocusLoss, limit, stacked, newestOnTop |
Swipe + hover-pause + stacking are default/expand; the fine-grained knobs are gone. |
| Full styling escape hatch | toast.custom(<Jsx/>) or unstyled — this is where custom containers land. |
Gotchas
- ⚠ Testing:
toast.promisemisbehaves under Jest-mocked promises (TypeError: n is not a function) ⭐ 13k [11]; jsdom needs awindow.matchMediamock or the<Toaster/>never renders. Sonner emits<li data-sonner-toast>inside anaria-live<ol>— everygetByRole('alert')/.Toastify__toastselector in your suite dies. - ⚠ Sonner is opinionated: no theming system beyond
theme/richColors/classNames. Heavily branded toastify containers = rewrite, not remap. - ⚠ i18n: both are string-in, so
t()calls port unchanged, but Sonner’saction/cancellabels and the close-buttonaria-labelneed<Toaster>-level translation.
Effort
Mechanical toastify→Sonner: 0.5–1 day for a typical app — swap imports, add <Toaster/>, sed the call sites, fix pending→loading. Budget +2–4 days if you have custom transitions, a manual progress bar, custom close buttons, or >20 toast assertions in tests. From toastr: same, plus dropping jQuery. The safe path both ways: a notify.ts wrapper module first, swap the engine behind it [12].