GodUIGodUI
129Follow on X

Anatomy of the Dynamic Island

How five fixed size presets, one stiff spring, and a separate crossfade combine into Apple's signature pill.

01/04Five presets
compact
140×36 · r18
default
220×44 · r22
long
360×52 · r26
tall
260×120 · r28
large
360×200 · r36
tsx
const SIZES: Record<DynamicIslandSize,{ width: number; height: number; radius: number }> = {compact: { width: 140, height: 36, radius: 18 },default: { width: 220, height: 44, radius: 22 },long: { width: 360, height: 52, radius: 26 },tall: { width: 260, height: 120, radius: 28 },large: { width: 360, height: 200, radius: 36 },};
01

Five presets

The Dynamic Island's shell and its content are animated by two separate systems inside the same element: the shell's box model is driven by layout + explicit animate, and the content inside it is a nested AnimatePresence that is unaware of the shape it sits in.

SIZES is a lookup table rather than a formula deriving a size from content, and every shape the shell can take is one of exactly five entries.

You can't pass a size you haven't defined: DynamicIslandSize is a union of these five keys, so the shell only ever animates between known, designed states.

02/04The shell spring
compactdefaultlongtalllarge
spring
animates
feel
520 / 32
width · height · radius
rubbery overshoot
tsx
// Underdamped so the shell settles with the signature rubbery overshoot.const SHELL_SPRING = {type: "spring" as const,stiffness: 520,damping: 32,};<motion.divlayoutinitial={false}animate={{  width: dims.width,  height: dims.height,  borderRadius: dims.radius,}}transition={reduceMotion ? { duration: 0 } : SHELL_SPRING}className="relative flex items-center justify-center overflow-hidden bg-black text-white shadow-xl"/>
02

The shell spring

Changing size triggers one animate call across all three box properties at once.

stiffness: 520 is much stiffer than the morph dialog's 320. The island snaps toward its target almost immediately and only overshoots by a touch, which is what makes it feel rubbery rather than soft. layout is set too, so if the island sits inside a flex or grid parent, siblings reflow around the resize instead of jumping. Tall and large aren't capsules anymore (their radius is much smaller than half the height), so the scene above springs the same three box properties the component does, rather than faking the morph with scale (which would warp the corners).

03/04The content crossfade
Exiting view
opacity 1→0 · scale 1→0.9 · blur 0→4px
Entering view
same curve, reversed, same 0.2s
tsx
const key = presenceKey ?? size;<AnimatePresence mode="popLayout" initial={false}><motion.div  key={key}  initial={reduceMotion ? false : { opacity: 0, scale: 0.9, filter: "blur(4px)" }}  animate={{ opacity: 1, scale: 1, filter: "blur(0px)" }}  exit={reduceMotion ? undefined : { opacity: 0, scale: 0.9, filter: "blur(4px)" }}  transition={{ duration: 0.2, ease: "easeOut" }}>  {children}</motion.div></AnimatePresence>
03

The content crossfade

The shell's animate only touches the box. What's inside crossfades through its own, independent AnimatePresence.

presenceKey defaults to size, so switching shapes is enough to trigger a crossfade. It is a separate prop so you can keep the same size and still crossfade to new content (a timer ticking inside a tall island, say) by changing presenceKey on its own. mode="popLayout" means the exiting and entering views animate concurrently rather than waiting their turn, which is what makes it read as one view dissolving into the next instead of two sequential fades.

04/04Result
IncomingAda
04

The result

Two separate systems: a five-preset shell spring for the box, and an independent crossfade for whatever you put inside it.

Cheap when idle

No observer, interval or subscription runs in the background. DynamicIsland only re-renders (and only animates) when its size or presenceKey prop changes. An island sitting at compact with a static dot inside costs nothing beyond the one style calculation React skips on every render where nothing changed.

Reduced motion

A single useReducedMotion() call gates both systems independently: the shell's transition collapses to { duration: 0 }, and the content's initial/exit become false/undefined so AnimatePresence swaps content instantly instead of interpolating opacity, scale, or blur. Either way the island still lands at the correct final size with the correct final content; only the motion is skipped.

Motion Score

Dynamic IslandDD: Layout-triggering
Dwidthsignature shared-layout container morph
Dheightsignature shared-layout container morph
Cborder-radiussignature shared-layout container morph
SopacityFade / cross-fade
SfilterFilter (blur / brightness)
Each property is graded by how the browser runs it, from S (composited off the main thread) down to F (layout thrashing); the component takes the worst. MotionScore methodology →