feat: persistent sonner functionality via localStorage
Problem
Some web-apps have critical messages that need to be seen, or a message that may be toasted during a page navigation or refresh. This PR adds message durability so certain toasts can be restored after a page refresh.
Summary
- Add
persistentoption to toast configuration - Implement localStorage-based persistence with SSR safety
- Normalize sonner id upon restoration with counter synchronization moved into class
- staggered animation for toast restoration
- customizable
storageKeyprop to Toaster component
Usage
Basic:
toast('Persistent Toast', { persistent: true })}
Works with evolving toasts, will update persistence if the toast updates:
toast.promise(
new Promise((resolve) => {
setTimeout(() => {
resolve({ name: 'Sonner' });
}, 2000);
}),
{
loading: 'Loading...',
success: (data: any) => ({
message: `${data.name} toast has been added`,
description: 'Custom description for the Success state',
}),
description: 'Global description',
persistent: true,
closeButton: true,
},
)
Restoration Compatibility
All types of toasts can be used with the persistent property. But, Each toast is stringified and then parsed when restored so obviously any functions or timeouts become disconnected.
Demo
https://github.com/user-attachments/assets/dc867d05-629b-4248-b8e8-995a3a2143d8
The latest updates on your projects. Learn more about Vercel for Git ↗︎
| Name | Status | Preview | Comments | Updated (UTC) |
|---|---|---|---|---|
| sonner | ✅ Ready (Inspect) | Visit Preview | 💬 Add feedback | Jul 28, 2025 2:35am |
This is nice, but I think this type of behavior should be coded by the consumer rather than be included in the library by default. What do you think?
Ya, having it integrated in the library reduces a good amount of boilerplate code and ensures a consistent experience across applications using the library.
But notably, there is one reliability aspect that this PR ensures. On loading persistent toasts we animate only the number of visibleToasts on load so it seems consistent with how the library animates new toasts. While persistent toasts load in we have already set the toastCounter so any new toasts do not overwrite persisted toasts. Since toastCounter cannot be set from outside the library logic, consumer code trying to implement this logic would not be able to guarantee a unique toast ID.
In summary: Both, animating persistent toasts in, and ensuring unique toast ID's is pretty non-obvious for consumer code to handle.
Very nice. I see what Emil is saying. It is quite comprehensive and feels like a really well-scoped integration for certain niches.
Nonetheless, having toast persist across routing, or whatever, is a very good addition. I would definitely consider it!
Maybe think the API/prop usage through a bit, keeping it as simple as possible. But I’d say +1.
IMO this should be included in the lib, not implemented by the user.