Toasts and Inline Feedback in Popups

Give users feedback inside a small extension popup: inline status next to controls, toasts with undo, accessible live regions, errors that explain what to do, feedback for actions that finish after the popup closes, and timing.

Published October 2, 2026 Updated October 2, 2026 8 min read
Table of Contents

The user clicks “Archive” in the popup and the item disappears. Did it archive, or delete? Can they undo it? They click “Sync now” and nothing visible happens for two seconds, so they click again. They click “Save” on a page the extension cannot read and the button just stops responding. A popup is a few hundred pixels square and closes the moment focus leaves it, so feedback has to be compact, immediate and accessible — and actions that finish after the popup closes need feedback somewhere else entirely. This guide covers inline status, toasts with undo, error messages, live regions and the popup-closed case. It belongs to popup interface design.

Choosing a feedback form

Feedback should appear where the user is looking and last as long as it is useful. Inline status — a short message or icon change next to the control — suits actions on a single control: “Saved”, a spinner inside a button, a checkmark. Toasts — a small bar at the bottom of the popup — suit actions that change the list or view, especially when an Undo makes sense. Inline errors sit next to the thing that failed and say what to do. Out-of-popup feedback — badge, notification — covers work that completes after the popup has closed. Whatever the form, screen reader users must hear it, which means an ARIA live region.

Feedback forms in a popupInline status, toast, inline error, badge and notification compared on where they appear, how long they last, and what they suit.FormWhereLastsSuitsInline statusNext to control~2 sToggles, savesToastBottom of popup4–6 s, pauses on hoverArchive, delete + UndoInline errorAt the failureUntil fixedCan't do X — here's whyBadgeToolbar iconBriefDone after closeNotificationOSUntil dismissedLong tasks
Near the control for small things; toast for list changes; outside the popup for late results.

Step-by-step: feedback that works in a popup

1. Add one live region

1<!-- popup.html -->
2<div id="announcer" class="visually-hidden" role="status" aria-live="polite"></div>
3<div id="toast-host" aria-live="off"></div>
1export function announce(text) {
2  const el = document.querySelector("#announcer");
3  el.textContent = "";
4  requestAnimationFrame(() => (el.textContent = text));
5}

Execution context: the popup. A single polite live region, present from page load, is announced reliably by screen readers; regions created at the moment of the message often are not. Clearing then setting on the next frame makes repeated identical messages (“Saved”, “Saved”) announce each time. The visual toast host is not itself live — the announcer speaks for it — to avoid double announcements. See announcing dynamic updates to screen readers.

2. Show progress inside the button

 1async function withButtonState(button, work) {
 2  const label = button.textContent;
 3  button.disabled = true;
 4  button.setAttribute("aria-busy", "true");
 5  button.dataset.state = "busy";
 6  try {
 7    await work();
 8    button.dataset.state = "done";
 9    button.textContent = chrome.i18n.getMessage("done");
10    announce(chrome.i18n.getMessage("syncDone"));
11  } catch (e) {
12    button.dataset.state = "error";
13    showInlineError(button, e);
14  } finally {
15    setTimeout(() => { button.textContent = label; button.disabled = false; button.removeAttribute("aria-busy"); delete button.dataset.state; }, 1500);
16  }
17}
18syncBtn.addEventListener("click", () => withButtonState(syncBtn, () => chrome.runtime.sendMessage({ type: "sync-now" })));

Execution context: the popup. Disabling the button while busy prevents double submission; a spinner via [data-state="busy"] CSS shows something is happening. Showing “Done” briefly in the button itself is the most local feedback possible. Respect reduced motion for the spinner, as in respecting reduced motion and forced colors.

A toast with undoArchiving an item removes it from the list optimistically and shows a toast with Undo; the actual archive is committed after the toast times out; pressing Undo restores the item and cancels the commit; closing the popup commits immediately.Click Archiveitem 42Remove from listoptimisticToast + Undo5 sthenUndo pressedrestore, cancelTimeoutcommit archivePopup closescommit now
Optimistic change, delayed commit, Undo in between.

3. Build a toast with undo

 1let current;
 2export function toast(message, { actionLabel, onAction, ms = 5000 } = {}) {
 3  current?.dismiss();
 4  const el = document.createElement("div");
 5  el.className = "toast";
 6  el.append(Object.assign(document.createElement("span"), { textContent: message }));
 7  let timer;
 8  const dismiss = () => { clearTimeout(timer); el.remove(); current = null; };
 9  if (actionLabel) {
10    const b = Object.assign(document.createElement("button"), { textContent: actionLabel });
11    b.addEventListener("click", () => { onAction(); dismiss(); });
12    el.append(b);
13  }
14  el.addEventListener("pointerenter", () => clearTimeout(timer));
15  el.addEventListener("pointerleave", () => (timer = setTimeout(dismiss, ms)));
16  el.addEventListener("focusin", () => clearTimeout(timer));
17  document.querySelector("#toast-host").append(el);
18  timer = setTimeout(dismiss, ms);
19  announce(actionLabel ? `${message}. ${actionLabel} available.` : message);
20  return (current = { dismiss });
21}

Execution context: the popup. One toast at a time keeps the small space readable. The timer pauses while the pointer or focus is on the toast, so users who reach for Undo don’t lose it. Keep toasts above the popup’s bottom edge and out of the way of the list’s last item.

4. Commit deferred actions safely

 1const pending = new Map();
 2
 3function archiveWithUndo(item) {
 4  removeFromList(item.id);
 5  const commit = () => { pending.delete(item.id); chrome.runtime.sendMessage({ type: "archive", id: item.id }); };
 6  const t = setTimeout(commit, 5000);
 7  pending.set(item.id, { t, commit });
 8  toast(chrome.i18n.getMessage("archived"), {
 9    actionLabel: chrome.i18n.getMessage("undo"),
10    onAction: () => { clearTimeout(t); pending.delete(item.id); restoreToList(item); announce(chrome.i18n.getMessage("restored")); },
11  });
12}
13
14addEventListener("pagehide", () => { for (const { t, commit } of pending.values()) { clearTimeout(t); commit(); } });

Execution context: the popup. Committing after the undo window is the simplest undo model, but the popup can close at any moment. pagehide fires when it does, and runtime.sendMessage dispatched there still reaches the service worker. Alternatively, commit immediately and implement Undo as a reverse operation — safer for important data, since a crash loses nothing.

Feedback for work that finishes after the popup closesThe user starts an export in the popup; the popup shows Started and closes; the worker finishes later and sets a checkmark badge for a few seconds or shows a notification for longer tasks.PopupService workerToolbar / OSstart export"Export started" — popup closes… export runs …badge ✓ (short) / notification (long)
When the popup is gone, the toolbar takes over.

5. Write errors that say what to do

 1function showInlineError(anchor, err) {
 2  const map = {
 3    "restricted-page": "errRestrictedPage",       // "Readable can't run on browser pages. Try it on an article."
 4    "no-site-access":  "errNoSiteAccess",         // "Readable doesn't have access to this site."
 5    "offline":         "errOffline",              // "You're offline. We'll sync when you're back."
 6  };
 7  const el = anchor.closest(".field, .row, section").querySelector(".error") ?? anchor.insertAdjacentElement("afterend", Object.assign(document.createElement("p"), { className: "error" }));
 8  el.textContent = chrome.i18n.getMessage(map[err?.code] ?? "errGeneric");
 9  el.id ||= `err-${crypto.randomUUID()}`;
10  anchor.setAttribute("aria-describedby", el.id);
11  announce(el.textContent);
12}

Execution context: the popup. Map error codes from the worker to messages that explain the cause in plain words and the next step. Link the error to its control with aria-describedby. For fixable errors, add the fix inline — a “Grant access” button for missing site access.

6. Hand off feedback when the popup closes

For actions that take longer than a second or two, acknowledge the start in the popup (“Export started”) and deliver completion through the badge or a notification from the service worker. See progress notifications for long tasks and running work that outlives the popup.

7. Keep timing humane

Inline confirmations: about 1.5–2 seconds. Toasts with actions: at least 5 seconds, paused on hover and focus. Errors: until fixed or dismissed. Never auto-dismiss something the user must act on.

Common mistakes

  • No feedback for silent actions. Users repeat them.
  • Live regions created on demand. Screen readers miss them.
  • Toasts that vanish under the cursor. Pause on hover and focus.
  • Undo lost when the popup closes. Commit on pagehide.
  • Generic “Something went wrong”. Say why and what to do.

Cross-browser variation

  • Chrome / Edge: pagehide fires when the popup closes; messages sent there reach the worker.
  • Firefox: popups behave similarly; test that pagehide work completes.
  • Safari: popovers close on blur too; keep deferred commits short or commit immediately.

Verification

  1. Archive an item, press Undo within five seconds, and confirm it returns and nothing was archived.
  2. Archive and close the popup immediately; confirm the archive was committed.
  3. With a screen reader, confirm “Saved”, toasts and errors are announced once each.
  4. Trigger each error code and confirm the message names the cause and fix.

FAQ

Should toasts stack?

Not in a popup. Replace the current toast; the space is too small for stacks.

Is a spinner enough for slow actions?

For up to a few seconds. Longer actions need progress or a handoff to the badge.

Should success always be announced?

Announce results the user cannot see change; skip announcing changes that move focus, which screen readers already report.

Where should toasts sit in a popup with a scrolling list?

Fix them to the bottom of the popup viewport, above the list, with enough padding that they never cover the last item’s controls. Popups do not resize to make room, so reserve the space with bottom padding on the list while a toast is visible.

Other UI/UX Patterns & Interactive Components Resources