sonner icon indicating copy to clipboard operation
sonner copied to clipboard

feat: persistent sonner functionality via localStorage

Open Erik-Koning opened this issue 5 months ago • 5 comments

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 persistent option 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 storageKey prop 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

Erik-Koning avatar Jul 28 '25 02:07 Erik-Koning

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

vercel[bot] avatar Jul 28 '25 02:07 vercel[bot]

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?

emilkowalski avatar Jul 31 '25 15:07 emilkowalski

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.

Erik-Koning avatar Jul 31 '25 19:07 Erik-Koning

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.

remcostoeten avatar Aug 14 '25 19:08 remcostoeten

IMO this should be included in the lib, not implemented by the user.

Fefedu973 avatar Sep 08 '25 16:09 Fefedu973