Undo Snackbar
An undo prompt bar with a countdown progress ring that confirms the action automatically when time runs out.
A snackbar that pops up from the bottom after a destructive action, holding a message, an undo button, and an SVG ring countdown (five seconds by default): pressing undo cancels the action and reports through onUndo, while the countdown reaching zero fires onCommit to make the action take effect for real. The countdown pauses automatically on pointer hover or keyboard focus. You can wire it up directly with controlled props, or use useUndoSnackbar for ready-made state management.
npx shadcn@latest add https://webberui.com/r/undo-snackbar.jsonPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<UndoSnackbar />
Installation
npx shadcn@latest add https://webberui.com/r/undo-snackbar.jsonOr, once registries are configured in components.json, install it as @webberui/undo-snackbar.
Usage
import { UndoSnackbar, useUndoSnackbar } from "@/components/ui/undo-snackbar";
function Inbox() {
const { show, snackbarProps } = useUndoSnackbar({ duration: 5000 });
const handleDelete = (item: Item) => {
hideItem(item.id); // optimistically remove it from the screen first
show(`Deleted "${item.title}"`, {
onUndo: () => restoreItem(item.id), // undo pressed within five seconds → restore
onCommit: () => permanentlyDelete(item.id), // timed out → delete for real
});
};
return (
<>
{/* ...list... */}
<UndoSnackbar {...snackbarProps} />
</>
);
}You can also skip the hook and manage the open state yourself with controlled props, closing it inside onUndo / onCommit.
Props
UndoSnackbar
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | — | Whether the snackbar is shown (controlled); close it inside onUndo / onCommit |
message | string | — | The snackbar message text |
duration | number | 5000 | Total countdown duration (milliseconds); reaching zero fires onCommit |
undoLabel | string | "復原" | Text of the undo button — the built-in default is the Traditional Chinese for "Undo", so pass this prop to localize it |
onUndo | () => void | — | Called when undo is pressed: cancels the action and closes |
onCommit | () => void | — | Called when the countdown ends: the action takes effect for real |
resetKey | number | string | — | When showing the same message repeatedly, changing this value restarts the countdown |
positioning | "fixed" | "absolute" | "fixed" | Snaps to the bottom of the window or of the nearest positioned ancestor |
className | string | — | Forwarded to the snackbar panel |
useUndoSnackbar
| Item | Type | Description |
|---|---|---|
options.duration | number | Total countdown duration (milliseconds), default 5000 |
options.undoLabel | string | Text of the undo button |
show(message, handlers?) | function | Shows the snackbar and registers onUndo / onCommit; any pending action still outstanding is committed immediately first |
close() | function | Closes silently, firing no callback |
snackbarProps | UndoSnackbarProps | Spread straight into <UndoSnackbar {...snackbarProps} /> |
Accessibility
- The snackbar uses
role="status"andaria-live="polite", so the message is announced automatically by the screen reader when it appears - The countdown pauses on pointer hover or keyboard focus (tabbing to the undo button) and only resumes once you leave, guaranteeing enough time to react
- Undo is a native
<button>with full keyboard support and afocus-visiblefocus ring - The ring countdown and the remaining seconds are visual aids and are hidden from assistive technology (
aria-hidden) - When the user has "reduce motion" enabled at the system level, the enter and exit become a plain fade in / fade out with no offset or spring animation