SnackbarProvider
Owns a queue of snackbars and renders them stacked at any screen position — wrap your app once, then call useSnackbar() anywhere to show one.
Overview
SnackbarProvider manages the actual queue of on-screen Snackbars: where they appear, how they stack when several are visible at once, and when they auto-dismiss. Wrap it around your app (or a section of it) once, then call useSnackbar() from any component beneath it to enqueue a toast.
Snackbars can be sent to any of 7 screen positions — the 4 corners, top-center, bottom-center, or dead-center — each position manages its own independent stack. When more than one snackbar is queued in the same position, the newest is fully visible up front; after a short delay the older ones animate into a compact stack peeking behind it. Hovering or focusing the stack re-expands it into a normal spaced list.
Import
import { SnackbarProvider, useSnackbar } from '@unflow.io/ui/components/SnackbarProvider';Basic usage
function NotifyButton() {
const { notify } = useSnackbar();
return (
<Button
label="Show a snackbar"
onClick={() =>
notify({
type: 'success',
title: 'Changes saved',
subtitle: 'Everything is up to date.',
position: 'bottomRight',
duration: 4000,
})
}
/>
);
}
function App() {
return (
<SnackbarProvider>
<NotifyButton />
</SnackbarProvider>
);
}SnackbarProvider props
| Prop | Type | Default | Description |
|---|---|---|---|
defaultPosition | SnackbarPosition | 'bottomRight' | Position used by notify() calls that don't specify one |
defaultDuration | number | — | Auto-dismiss duration (ms) used by notify() calls that don't specify one. Omit to persist by default |
maxPerPosition | number | — | Once a position holds more than this many snackbars, the oldest is dropped. Omit for unlimited |
SnackbarPosition is one of: 'topLeft' | 'topCenter' | 'topRight' | 'bottomLeft' | 'bottomCenter' | 'bottomRight' | 'center'.
useSnackbar()
| Return value | Type | Description |
|---|---|---|
notify | (options) => string | Enqueues a snackbar and returns its id |
update | (id: string, patch) => void | Patches a still-queued snackbar — e.g. to drive progress yourself over time. No-ops once it's been dismissed |
dismiss | (id: string) => void | Dismisses a specific snackbar early |
dismissAll | (position?: SnackbarPosition) => void | Dismisses every queued snackbar, optionally scoped to one position |
notify() accepts every Snackbar prop except onClose (which the provider wires up to remove the item from the queue — see below), plus:
| Option | Type | Description |
|---|---|---|
position | SnackbarPosition | Defaults to the provider's defaultPosition |
duration | number | Auto-dismiss after this many ms. Omit (and the provider's defaultDuration) to persist until manually closed — e.g. to force the user to act on it |
onClose | () => void | Called when this snackbar is dismissed, for any reason — close click, auto-dismiss, or a manual dismiss(id) call |
progress is never computed automatically by the provider — pass a static value to notify(), or call update(id, { progress }) yourself over time (see below).
Stacking multiple snackbars
function StackButton() {
const { notify } = useSnackbar();
const countRef = useRef(0);
const fire = () => {
countRef.current += 1;
notify({ position: 'bottomRight', title: `Notification #${countRef.current}` });
};
return <Button label="Add a snackbar" onClick={fire} />;
}Driving progress yourself
progress is always controlled by you, not the provider — call update(id, { progress }) on your own timer:
function UploadButton() {
const { notify, update } = useSnackbar();
const startUpload = () => {
const id = notify({ type: 'dark', title: 'Uploading file.zip', progress: 0 });
let progress = 0;
const interval = setInterval(() => {
progress = Math.min(100, progress + 10);
update(id, { progress });
if (progress >= 100) clearInterval(interval);
}, 300);
};
return <Button label="Start upload" onClick={startUpload} />;
}Auto-dismiss with a countdown
Set duration to auto-dismiss a snackbar, and pair it with update() to visually track the time remaining — the interval is cleared via onClose, which fires no matter how the snackbar goes away (timeout, close click, or a manual dismiss(id)):
function TimedButton() {
const { notify, update } = useSnackbar();
const fire = () => {
const duration = 5000;
const startedAt = Date.now();
let interval: ReturnType<typeof setInterval>;
const id = notify({
type: 'dark',
title: 'This will disappear in 5s',
subtitle: 'The bar tracks time remaining, driven by the caller.',
duration,
progress: 100,
onClose: () => clearInterval(interval),
});
interval = setInterval(() => {
const elapsed = Date.now() - startedAt;
const remaining = Math.max(0, 100 - (elapsed / duration) * 100);
update(id, { progress: remaining });
if (remaining <= 0) clearInterval(interval);
}, 100);
};
return <Button label="Show timed snackbar" onClick={fire} />;
}Limiting the queue
Pass maxPerPosition to the provider to cap how many snackbars can be visible in one position at a time — once exceeded, the oldest is dropped automatically:
<SnackbarProvider maxPerPosition={3}>{children}</SnackbarProvider>Accessibility
- Each queued position is a separate landmark region; only positions with at least one active snackbar are rendered.
- Snackbars themselves use
role="status"(see Snackbar), so they're announced without stealing focus. - The stack container doesn't intercept pointer events except over the snackbars themselves, so it never blocks interaction with content underneath.
- Hovering or focusing the stack expands it — keyboard users tabbing into an older, mostly-hidden snackbar's action button will see the whole stack spread out first.