Persistent Toasts

Toasts almost always disappear on a timer. You push a notification, the user has a few seconds to read it, and then it slides away. That's fine when the message is ephemeral — saved, copied, sent — but it's awkward when the toast is actually carrying state you want to keep around: a long-running deploy, a queue of items waiting on review, a list of background jobs that are still running.

Once toasts are persistent, the design gets harder. They pile up. You need a way to read what's there, dismiss them individually, dismiss the whole pile at once, and not have the stack take over the screen. The "deck of cards that fans on hover" pattern — popularised by Sonner for ephemeral toasts — turns out to fit the persistent case really well too, with a few extra interactions added on top.

Push a few and click the deck:

What's happening

Three modes, smoothly interpolated between:

  1. Deck — only the front card is fully visible. The next two peek out 8 and 16 pixels behind it, scaled down. Anything older sits invisible at the bottom of the stack. The deck telegraphs that more notifications exist without dominating the screen.
  2. Hovered deck — the gap between cards widens slightly so you can feel that the stack is interactive before you've actually clicked it. A small move that makes the click target feel honest.
  3. Expanded — click the deck and every toast fans out into a vertical list. Each card has its own dismiss button on hover, and a bulk "remove all" button slides in alongside the collapse control so you can blow the whole list away in one go.

Compared to a Sonner-style auto-dismiss toaster, the additions are mostly affordances that come from making the toasts persistent: bulk dismiss, individual dismiss, scrolling once the list outgrows the viewport, and a click-to-expand because the user has to take an action to read the older toasts (they're not going to wait it out).

The animation logic, (after a few rewrites)

The root is a container that switches its display mode based on whether the toaster is expanded:

<motion.div
  layout
  style={{
    display: expanded ? 'flex' : 'grid',
    flexDirection: 'column',
    gap: 12,
    gridTemplateAreas: '"stack"',
    gridTemplateColumns: '1fr',
  }}
>
  {toasts.map((toast, index) => <Toast ... />)}
</motion.div>

In collapsed mode it's a grid with a single "stack" cell — every toast slots into that one cell so they overlap in the same spot, and the cell sizes to whichever toast is tallest. In expanded mode it flips to a flex column with a 12px gap and the browser flows the toasts one under the other. The layout prop on the container is what makes the size transition animate instead of snap: framer-motion measures the bounding box before and after each render and FLIPs the difference (first / last / invert / play).

That choice cascades down into each toast:

<motion.div
  layout="position"
  animate={{
    y: expanded ? 0 : index * 8,
    scale: expanded ? 1 : 1 - index * 0.05,
  }}
  style={{
    gridArea: 'stack',
    zIndex: count - index,
  }}
/>

gridArea: 'stack' is what places every toast into that shared cell when the parent is grid. When the parent flips to flex (expanded mode), gridArea is ignored and each toast just takes its natural place in the column. Visual stacking in the collapsed deck is decided by zIndex — front toast on top. layout="position" animates the bounding-box change between the two regimes.

The y and scale in animate are doing the deck-specific work that layout can't: pushing deeper cards down a few pixels to peek out from behind the front, and shrinking each one a touch. Both go to their identity values (0 and 1) when expanded so they don't fight the flex flow.

Where it fits

Anywhere the "latest item matters most but the older ones shouldn't disappear" rule applies and the items are persistent: a notification inbox, a queue of background jobs, a recent-activity panel, a list of pending reviews. If your toasts auto-dismiss, you can use the simpler version — swap click-to-expand for hover, drop the bulk dismiss, and you've got something close to Sonner. If they persist, you need the extra interactions on top, but the deck-and-fan idiom still pulls its weight.