Skip to content
WANCRAFT

D.47Navigation & Menus

Dynamic Island

A pill that morphs between idle, compact and expanded live activities on a spring.

NewReactMotionTailwindadvanced

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 verified

Uses 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.json

Method 02 · Copy and paste

  1. 1. Install the dependencies below.
  2. 2. Copy the source from the Code tab into components/ui/dynamic-island.tsx.
  3. 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.

example.tsx
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

PropTypeDefaultDescription
viewsRecord<K, DynamicIslandView>—{ label: string; compact: ReactNode; expanded: ReactNode; announcement?: string } per activity.
activityK | null—The current activity key, or null for the idle pill.
expandedboolean—Expanded state (controlled).
defaultExpandedbooleanfalseInitial 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.
blurbooleantrueBlur content while it cross-fades.
idleWidthnumber120Width of the idle pill in px.
expandedWidthnumber340Width of the expanded card in px.
accentstring"#FF7448"Focus ring colour, exposed to views as --di-accent.
...propsHTMLAttributes<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
motion · springmotion · layoutinput · clickinput · keyboardinput · focus

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.