Overview
Dynamic Island is a black capsule that hosts live activities such as a music player, a timer or an upload. You pass a typed record of views (each with compact and expanded content) and the current activity; the island measures whichever content is on screen with a ResizeObserver and springs its width, height and corner radius to fit, while the old content blurs out and the new one fades in. Clicking or pressing Enter on the compact pill expands it and moves focus inside, Escape or a press outside collapses it, and each change is announced through a polite live region.
01Installation
Method 01 · Registry
CLI verifiedUses the shadcn CLI in a project initialised with shadcn init. Installs the file to components/ui/dynamic-island.tsx and adds its dependencies.
npx shadcn@latest add https://wancraft.my/r/dynamic-island.jsonMethod 02 · Copy and paste
- 1. Install the dependencies below.
- 2. Copy the source from the Code tab into
components/ui/dynamic-island.tsx. - 3. Import it and adjust props.
npm install motion- Dependencies
- motion
- Peer
- react ≥ 18, tailwindcss ≥ 4
- Added
- 13 Oct 2026
- Updated
- —
02Usage
The default configuration. Change properties above to generate a tailored example in the Usage tab.
import { DynamicIsland, type DynamicIslandView } from "@/components/ui/dynamic-island";
export function DynamicIslandExample() {
const views: Record<"timer", DynamicIslandView> = {
timer: {
label: "Timer",
compact: (
<>
<span>Timer</span>
<span className="tabular-nums">04:59</span>
</>
),
expanded: <p className="text-3xl tabular-nums">04:59</p>,
},
};
return (
<DynamicIsland views={views} activity="timer" />
);
}03Props
| Prop | Type | Default | Description |
|---|---|---|---|
| views | Record<K, DynamicIslandView> | — | { label: string; compact: ReactNode; expanded: ReactNode; announcement?: string } per activity. |
| activity | K | null | — | The current activity key, or null for the idle pill. |
| expanded | boolean | — | Expanded state (controlled). |
| defaultExpanded | boolean | false | Initial expanded state (uncontrolled); collapses when the activity changes. |
| onExpandedChange | (expanded: boolean) => void | — | Called when the island asks to expand or collapse. |
| spring | { stiffness: number; damping: number; mass: number } | { stiffness: 400, damping: 30, mass: 0.9 } | Spring that morphs size and corner radius. |
| blur | boolean | true | Blur content while it cross-fades. |
| idleWidth | number | 120 | Width of the idle pill in px. |
| expandedWidth | number | 340 | Width of the expanded card in px. |
| accent | string | "#FF7448" | Focus ring colour, exposed to views as --di-accent. |
| ...props | HTMLAttributes<HTMLDivElement> | — | Native attributes are forwarded to the root. |
04Features
- 01Typed views record: the activity prop only accepts keys of your views
- 02Content-measured morphing: width, height and radius spring to whatever is on screen
- 03Blur + scale cross-fade between states with popLayout, so old and new content overlap smoothly
- 04Controlled or uncontrolled expansion; a new activity starts collapsed
- 05Escape or a press outside collapses the expanded card
05Accessibility
- The compact pill is a single <button> with aria-expanded=false, named "<label>: expand" and described by the compact content.
- Expanding moves focus into a labelled group; Escape collapses and returns focus to the pill.
- A visually hidden Collapse button (visible on focus) gives keyboard and screen reader users an explicit way back.
- Activity changes and expansion are announced through a polite live region; the idle pill is hidden from assistive tech.
- With prefers-reduced-motion the island resizes instantly and content swaps with no blur or scale.
06Limitations
- Compact content must not contain interactive elements, because the whole compact pill is a button.