shadimage
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 · medium
animationmotion

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))
}

More from easeui

Browse all components in this library →