# Instant actions, undo and toasts

> Make likely-success actions feel instant, choose between undo and a confirmation for destructive ones, and use toasts (and Sonner) without the usual bugs.

Module: Components and states · Level: intermediate · 9 min · https://school.agents.org.in/design/components/optimistic-and-undo

Both hearts get tapped once as they scroll in. Same server, same 700ms delay; tap them again yourself.

*[Interactive demo: Left waits for the server. Right updates now and only rolls back if the server says no.]*

The right one feels instant because it *is* instant, as far as you can tell. That is an optimistic update (Changing the screen right away as if an action succeeded, sending the request in the background, and rolling back only if it fails. It makes likes and toggles feel instant): change the screen immediately, send the request in the background, and undo the change only if it fails. Tick "Make the server fail" to watch the rollback.

It is the single biggest speed win available without touching the servers.

## When to be optimistic

```ts
const previous = liked;
setLiked(!liked);                // update now
try {
  await api.toggleLike(post.id); // confirm in the background
} catch {
  setLiked(previous);            // roll back
  toast.error("Couldn't update the like. Try again.");
}
```

| Be optimistic | Don't |
|---|---|
| Toggles: like, bookmark, follow, mute | Payments and bookings: the confirmation *is* the design |
| Marking a to-do complete | Anything only the server can validate, like "is this username free?" |
| Renaming with client-side checks | Sign-up and login, where a failure is information |
| Reordering a list | Destructive actions **without** undo |

If the server creates an id, give the new item a temporary client id and swap it when the response lands. And test the rollback path on purpose: a rollback that yanks the page somewhere else is worse than a 200ms spinner.

## Undo or confirm?

Now the scary actions. Archive a project below, then try deleting the whole workspace.

*[Interactive demo: Archive acts at once with undo. Delete workspace can't be undone, so it asks you to type its name.]*

A confirmation on every delete trains people to click through it, so the one that matters gets clicked through too. Decide first:

| The action | Pattern |
|---|---|
| Reversible and frequent: archive, move, delete to trash | Do it now; offer **Undo** in the result |
| Irreversible, one item | Confirm |
| Irreversible, many items | Confirm, with the count |
| Affects other people | Confirm, and say who: "The 8 members lose access" |
| An account, workspace or repository | Type its name to confirm; the button stays disabled until it matches |

Undo is cheaper for the user and more honest. It is also the optimistic pattern: remove the row now, and let the real delete run during the undo window.

## A confirmation you can answer without reading the body

  **Before (avoid):**

**Are you sure?**<br />This action cannot be undone.<br />`OK` `Cancel`

  **After (do this):**

**Delete workspace "acme-web"?**<br />This permanently deletes 4 projects and their 212 files.<br />`Cancel` `Delete workspace`

- **Title** names the action and the object. Never "Are you sure?".
- **Body** says what is lost, with counts.
- **Buttons** are verb plus object, never Yes and No.
- **Focus starts on Cancel**, so a reflexive Enter does no harm.
- **Only destruction is red.** If the brand itself is red, give destruction a different treatment (outlined, with an icon).

*(Delete sitting in a menu right under Rename, with no gap, no color and no undo, is the most common unguarded delete there is.)*

## The undo toast is not a normal toast

A toast (A small message that slides in at the edge of the screen to confirm something ("Changes saved") and leaves on its own. One with an Undo button has to stay longer and pause while you look at it) is a small message that slides in at the edge of the screen and leaves on its own. The undo toast carries an action, so it plays by stricter rules: at least 5 seconds, **paused while hovered or focused**, and announced politely without stealing focus. An undo link that vanishes on a timer is data loss on a schedule.

Try the stack: fire a few, then hover or tap it.

*[Interactive demo: Newest in front. The stack spreads and every timer pauses while you look.]*

A plain "Changes saved" can leave after 4 seconds. An error that needs action stays until dismissed. Reading time scales with the words (about 200 to 250 words a minute), never a flat 2 seconds.

## Sonner in practice

Sonner, by Emil Kowalski, is the toast library most React apps reach for. It needs exactly two pieces: **one** `Toaster` component mounted once near the root (the root layout in Next.js), and `toast()` called from client code.

| Need | Call |
|---|---|
| A message with a second line | `toast("Saved", { description })` |
| A typed icon | `toast.success`, `.error`, `.info`, `.warning` |
| Tied to a promise | `toast.promise(p, { loading, success, error })` |
| Update a toast later | `const id = toast.loading("…")`, then `toast.success("Done", { id })` |
| A button | `action: { label: "Undo", onClick }` |
| Your own design | `toast.custom((t) => …)` |

The bugs are almost always one of these:

| Symptom | Cause |
|---|---|
| Nothing appears | No Toaster mounted, or `toast()` ran on the server |
| Appears twice | Two Toasters (layout and page), or a React effect ran twice; fire from the click handler or pass a stable `id` |
| Hidden behind a modal | An ancestor created a stacking context; mount the Toaster at the document root |
| Stuck on loading | The promise never settles |
| "Dark mode is broken" | `theme` defaults to `"light"`; pass `theme="system"`. Colors stay gray until `richColors` is set |

**Optimistic with undo (paste into your agent):**

```text
Make archiving instant: remove the row immediately, then call the API. Show a Sonner toast "Archived 'NAME'." with an Undo action that restores the row at its old position and cancels the request.
- The toast stays at least 6s, pauses on hover and focus, and does not take focus.
- If the API fails, restore the row and toast.error("Couldn't archive. Restored.").
- Move focus to the next row's action (or the previous one) when the focused row disappears.
- One Toaster, with theme="system" and richColors, in the root layout only.
For deleting the workspace, use our dialog: title "Delete workspace 'NAME'?", counts in the body, Cancel focused, "Delete workspace" disabled until the name is typed.
```

**Check yourself:** Which action should get an undo toast instead of a confirmation dialog?

- [ ] Deleting a whole workspace and its repositories
- [x] Archiving one email from the inbox
- [ ] Charging a customer's card
- [ ] Removing every member from a team

*Why:* Archiving is frequent and reversible, so act at once and offer undo. Irreversible or many-item actions, and anything involving money, get a confirmation.

## In the skill

- https://school.agents.org.in/skill/components/optimistic-updates.md
- https://school.agents.org.in/skill/components/destructive-actions.md
- https://school.agents.org.in/skill/components/sonner-usage.md
