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.
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.
<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
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | — | App content; useDynamicIslandToast() must be called within this scope |
duration | number | 4000 | Milliseconds before auto-dismiss; set to 0 or below, or Infinity, to disable auto-dismiss |
max | number | 4 | Maximum number of notifications kept at once; beyond that, the oldest one is pushed out |
align | 'start' | 'center' | 'end' | 'center' | Horizontal alignment of the island |
offset | number | 16 | Offset from the top of the container (px) |
persistent | boolean | true | Whether to keep the idle pill (with its breathing dot) when there are no notifications |
container | HTMLElement | null | — | Render target container; when provided it mounts absolute (the container needs relative), otherwise it mounts fixed to document.body |
useDynamicIslandToast()
Returns { notify, update, dismiss }:
| Method | Signature | Description |
|---|---|---|
notify | (options: IslandToastOptions) => string | Pushes a notification and returns its id |
update | (id: string, options: Partial<IslandToastOptions>) => void | Updates an existing notification by id and restarts its countdown |
dismiss | (id: string) => void | Manually closes a notification by id |
IslandToastOptions
| Field | Type | Default | Description |
|---|---|---|---|
title | string | — | Title (the primary message, truncated to one line) |
description | string | — | Description text, shown below the title |
variant | 'default' | 'success' | 'error' | 'loading' | 'default' | Leading icon (Bell / CheckCircle2 / XCircle / spinning Loader2) |
icon | React.ReactNode | — | Custom leading icon, overriding the variant default icon |
duration | number | inherits from Provider | Overrides 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'slayoutlets the island stretch to the content size with a spring, while the content crossfades throughAnimatePresence(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), untilupdate()swaps in a result ordismiss()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
setTimeoutis 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 bymax, or the Provider unmounts, its timer is always reclaimed. - Mounting in place: by default it is
fixedand centered against the top of the window; passingcontainerswitches it toabsoluteinside 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 carriesrole="status", so new notifications and content changes fromupdate()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
Drawer
A panel that slides in from the edge of the screen, supporting all four directions, click-the-overlay to close, and single-axis drag to swipe it away.
Toast Stack
sonner-style collapsing toast stack — new toasts slide in from the edge, collapse into a pile of cards when not hovered, and expand into the full list while pausing the auto-dismiss countdown when the pointer enters the viewport.