WebberUI

Dynamic Island Toast

An iOS Dynamic Island-style notification — an idle pill expands fluidly into a notification card, multiple notifications stack behind it with their edges peeking out, and a loading state can update in place to success.

This is a WebberUI Pro component

Free during the launch campaign: sign up or sign in, then hit “Copy install command” in the preview above and it installs straight away — no payment, no credit card. The command below returns 401 while you are signed out.

How to install Pro components →See the plans →

Loading preview…
npx shadcn@latest add "https://webberui.com/r/dynamic-island-toast.json?t=<install token>"

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

4000
4
16
<DynamicIslandToast />

Installation

npx shadcn@latest add "https://webberui.com/r/dynamic-island-toast.json?t=<install token>"

Or, once registries are configured in components.json, install it as @webberui/dynamic-island-toast.

Usage

Wrap DynamicIslandToastProvider once at the outermost level of your app (or around whatever scope needs notifications); any component inside can then push notifications through useDynamicIslandToast():

import {
  DynamicIslandToastProvider,
  useDynamicIslandToast,
} from "@/components/ui/dynamic-island-toast";

function App() {
  return (
    <DynamicIslandToastProvider duration={4000} max={4}>
      <SaveButton />
    </DynamicIslandToastProvider>
  );
}

function SaveButton() {
  const { notify } = useDynamicIslandToast();

  return (
    <button
      onClick={() =>
        notify({
          title: "Deploy complete",
          description: "Live on production",
          variant: "success",
        })
      }
    >
      Deploy
    </button>
  );
}

notify() returns the id of that notification; combined with update() you can swap the same loading notification in place for its result, while dismiss() closes it early:

const { notify, update, dismiss } = useDynamicIslandToast();

const id = notify({ title: "Syncing…", variant: "loading" });
// Flow finished: the same island morphs straight into success instead of opening another one
update(id, { title: "Sync complete", variant: "success" });
// Or close it manually
dismiss(id);

The island itself is built into DynamicIslandToastProvider and is by default mounted fixed to document.body via createPortal; passing container (which needs position: relative) mounts it absolute inside that container instead, which suits keeping it confined to a demo section.

Props

DynamicIslandToastProvider

PropTypeDefaultDescription
childrenReact.ReactNodeApp content; useDynamicIslandToast() must be called within this scope
durationnumber4000Milliseconds before auto-dismiss; set to 0 or below, or Infinity, to disable auto-dismiss
maxnumber4Maximum number of notifications kept at once; beyond that, the oldest one is pushed out
align'start' | 'center' | 'end''center'Horizontal alignment of the island
offsetnumber16Offset from the top of the container (px)
persistentbooleantrueWhether to keep the idle pill (with its breathing dot) when there are no notifications
containerHTMLElement | nullRender target container; when provided it mounts absolute (the container needs relative), otherwise it mounts fixed to document.body

useDynamicIslandToast()

Returns { notify, update, dismiss }:

MethodSignatureDescription
notify(options: IslandToastOptions) => stringPushes a notification and returns its id
update(id: string, options: Partial<IslandToastOptions>) => voidUpdates an existing notification by id and restarts its countdown
dismiss(id: string) => voidManually closes a notification by id

IslandToastOptions

FieldTypeDefaultDescription
titlestringTitle (the primary message, truncated to one line)
descriptionstringDescription text, shown below the title
variant'default' | 'success' | 'error' | 'loading''default'Leading icon (Bell / CheckCircle2 / XCircle / spinning Loader2)
iconReact.ReactNodeCustom leading icon, overriding the variant default icon
durationnumberinherits from ProviderOverrides the auto-dismiss milliseconds for this notification

How it works

  • Fluid morphing: the idle pill and the notification card are the same motion.div. Motion's layout lets the island stretch to the content size with a spring, while the content crossfades through AnimatePresence (popLayout), producing the Dynamic Island feel of one black blob flowing and reshaping.
  • Stacked depth: the foreground is always the newest notification; older ones sit behind the island, absolutely positioned so a thin sliver peeks out below (each layer translateY 7px, scale −0.05, decreasing opacity), for at most 2 more visible layers — the rest are counted but not rendered.
  • loading lifecycle: variant: "loading" persists without a countdown by default (showing the spinning icon), until update() swaps in a result or dismiss() closes it; update() lets the same island morph directly instead of opening a new notification and causing a flicker.
  • Countdown pausing: when the pointer enters or keyboard focus moves into the island, every setTimeout is cleared; once it leaves, a full countdown is scheduled again for each non-loading notification (reset, not resumed). Whenever a notification is dismissed, pushed out by max, or the Provider unmounts, its timer is always reclaimed.
  • Mounting in place: by default it is fixed and centered against the top of the window; passing container switches it to absolute inside the given container, which makes it easy to demo inside a section in the document flow.

Accessibility

  • The island container carries aria-live="polite" and each notification carries role="status", so new notifications and content changes from update() are announced by the screen reader without interrupting what the user is doing
  • Every notification has a close button with an aria-label; keyboard focus entering the island behaves like hover — the countdown pauses, and resumes once focus leaves
  • The idle pill's breathing dot and the stacked edges are all aria-hidden, so they are not read out by assistive technology
  • When the user has "reduce motion" enabled at the system level, the morphing, the stack offsets, and the breathing dot all drop to zero and degrade to a brief fade in / fade out with no sense of movement
  • Notifications never steal focus; after a push, focus stays on whatever element the user was working with

On this page