UnflowUI
Components

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

PropTypeDefaultDescription
defaultPositionSnackbarPosition'bottomRight'Position used by notify() calls that don't specify one
defaultDurationnumberAuto-dismiss duration (ms) used by notify() calls that don't specify one. Omit to persist by default
maxPerPositionnumberOnce 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 valueTypeDescription
notify(options) => stringEnqueues a snackbar and returns its id
update(id: string, patch) => voidPatches a still-queued snackbar — e.g. to drive progress yourself over time. No-ops once it's been dismissed
dismiss(id: string) => voidDismisses a specific snackbar early
dismissAll(position?: SnackbarPosition) => voidDismisses 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:

OptionTypeDescription
positionSnackbarPositionDefaults to the provider's defaultPosition
durationnumberAuto-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() => voidCalled 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.