easeui
componentdynamic-islandsource: ok · hit
Dynamic Island
A pill that morphs between a compact status line and any number of named live-activity views, the same element reshaping via layout animation each time.
npx shadcn add "https://easeui.dev/r/dynamic-island.json"free
Where it goes
stats
Interaction
animated-counter
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · mediumanimationmotion
Files3 · source cached 10/8/2026, 9:04:56 PM
components/motion/dynamic-island.tsx
"use client";
// easeui.dev/components/motion/dynamic-island
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { Children, isValidElement, type ReactElement, type ReactNode } from "react";
import { EASE_OUT } from "@/lib/ease";
import { cn } from "@/lib/utils";
export interface DynamicIslandViewProps {
/** Matches the parent's `view` prop when this is the active view. */
id: string;
className?: string;
children?: ReactNode;
}
/** A named live-activity view. Read by the parent DynamicIsland; never rendered on its own. */
export function DynamicIslandView({ children }: DynamicIslandViewProps) {
return <>{children}</>;
}
export interface DynamicIslandProps {
/** Which view is active. `null` shows the compact pill. */
view: string | null;
/** Compact pill content, shown while no view is active. */
compact?: ReactNode;
/** One or more DynamicIslandView elements. */
children?: ReactNode;
className?: string;
}
// A deliberate exception to this library's usual bounce:0 springs — the
// island is meant to feel soft and a little elastic, like it's made of
// liquid, not a stiff panel. Reduced motion drops the bounce entirely below.
const MORPH_SPRING = { type: "spring", bounce: 0.35, duration: 0.6 } as const;
/**
* A pill that morphs between a compact status line and any number of named
* live-activity views — the same element reshaping via layout animation
* each time, rather than one panel swapping for another beside it.
*/
export function DynamicIsland({ view, compact, children, className }: DynamicIslandProps) {
const reduce = useReducedMotion();
const views = Children.toArray(children).filter(isValidElement) as ReactElement<DynamicIslandViewProps>[];
const active = views.find((item) => item.props.id === view);
return (
<motion.div
layout={!reduce}
transition={reduce ? { duration: 0 } : MORPH_SPRING}
style={{ borderRadius: active ? 28 : 9999, willChange: "transform" }}
className={cn(
"mx-auto flex w-fit max-w-full items-center justify-center overflow-hidden bg-foreground text-background",
className,
)}
>
<AnimatePresence mode="popLayout" initial={false}>
{active ? (
<motion.div
key={active.props.id}
layout
initial={reduce ? false : { opacity: 0 }}
animate={{ opacity: 1, transition: { delay: reduce ? 0 : 0.08, duration: 0.15, ease: EASE_OUT } }}
exit={reduce ? undefined : { opacity: 0 }}
className={cn("flex items-center gap-3 p-4", active.props.className)}
>
{active.props.children}
</motion.div>
) : (
<motion.div
key="compact"
layout
initial={reduce ? false : { opacity: 0 }}
animate={{ opacity: 1 }}
exit={reduce ? undefined : { opacity: 0 }}
transition={{ duration: 0.15, ease: EASE_OUT }}
className="flex items-center gap-2 px-4 py-2 text-sm font-medium"
>
{compact}
</motion.div>
)}
</AnimatePresence>
</motion.div>
);
}
lib/ease.ts
// Shared motion tokens. Micro-interactions run 100 to 150ms, standard UI
// 150 to 250ms, and panels up to 300ms.
// Easing curves mirror the CSS custom properties in globals.css.
/** ease-out-quint. Fast start that settles quickly. Entrances, exits, feedback. */
export const EASE_OUT = [0.23, 1, 0.32, 1] as const;
/** ease-in-out-cubic. Elements already on screen moving to a new spot. */
export const EASE_IN_OUT = [0.645, 0.045, 0.355, 1] as const;
/** Sheet and drawer glide. */
export const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;
/** CSS string form of EASE_OUT for inline style transitions. */
export const EASE_OUT_CSS = "cubic-bezier(0.23, 1, 0.32, 1)";
// Springs are described by duration and bounce, which is easier to reason about
// than stiffness and damping. Bounce stays at zero for product UI.
/** Press feedback on buttons and other tappable surfaces. */
export const SPRING_PRESS = {
type: "spring",
duration: 0.15,
bounce: 0,
} as const;
/** Content swaps, label and icon slots trading places inside a control. */
export const SPRING_SWAP = {
type: "spring",
duration: 0.2,
bounce: 0,
} as const;
/** Overlay panel entrances, modals and sheets summoned by pointer. */
export const SPRING_PANEL = {
type: "spring",
duration: 0.25,
bounce: 0,
} as const;
/** Shared-layout glides, pills and indicators moving between positions. */
export const SPRING_LAYOUT = {
type: "spring",
duration: 0.22,
bounce: 0,
} as const;
/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt). */
export const SPRING_MOUSE = {
stiffness: 320,
damping: 26,
mass: 0.3,
} as const;
/** Dragged handles and fills (sliders). Critically damped `useSpring` config,
* so the value follows the pointer closely and never rebounds off an end. */
export const SPRING_GLIDE = {
stiffness: 700,
damping: 50,
mass: 0.5,
} as const;
lib/utils.ts
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}