All components

Dynamic Island

Dynamic Island is a React and Tailwind CSS component from SmoothUI, licensed MIT. Copy it and paste it into your project.

SmoothUI MIT other

What it is

Dynamic Island is a React and Tailwind CSS component from SmoothUI, licensed MIT. Copy it and paste it into your project.

How to use it

Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.

Open in the catalogue

Five free copies a month. No card.

Needs

npm i motion lucide-react

Licence

This component comes from SmoothUI and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.

Dynamic Island

index.tsx
"use client";

import {
  Bell,
  CloudLightning,
  Music2,
  Pause,
  Phone,
  Play,
  SkipBack,
  SkipForward,
  Thermometer,
  Timer as TimerIcon,
} from "lucide-react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { type ReactNode, useMemo, useState } from "react";

const BOUNCE_VARIANTS = {
  idle: 0.5,
  "idle-ring": 0.5,
  "idle-timer": 0.3,
  "ring-idle": 0.5,
  "ring-timer": 0.35,
  "timer-idle": 0.3,
  "timer-ring": 0.35,
} as const;

const DEFAULT_BOUNCE = 0.5;
const TIMER_INTERVAL_MS = 1000;

// Idle Component with Weather
const DefaultIdle = () => {
  const [showTemp, setShowTemp] = useState(false);

  return (
    <motion.div
      className="flex items-center gap-2 px-3 py-2"
      layout
      onHoverEnd={() => setShowTemp(false)}
      onHoverStart={() => setShowTemp(true)}
    >
      <AnimatePresence mode="wait">
        <motion.div
          animate={{ opacity: 1, scale: 1 }}
          className="text-white"
          exit={{ opacity: 0, scale: 0.8 }}
          initial={{ opacity: 0, scale: 0.8 }}
          key="storm"
        >
          <CloudLightning className="h-5 w-5 text-white" />
        </motion.div>
      </AnimatePresence>

      <AnimatePresence>
        {showTemp ? (
          <motion.div
            animate={{ opacity: 1, width: "auto" }}
            className="flex items-center gap-1 overflow-hidden text-white"
            exit={{ opacity: 0, width: 0 }}
            initial={{ opacity: 0, width: 0 }}
          >
            <Thermometer className="h-3 w-3" />
            <span className="pointer-events-none whitespace-nowrap text-white text-xs">
              12°C
            </span>
          </motion.div>
        ) : null}
      </AnimatePresence>
    </motion.div>
  );
};

// Ring Component
const DefaultRing = () => (
  <div className="flex w-64 items-center gap-3 overflow-hidden px-4 py-2 text-white">
    <Phone className="h-5 w-5 text-green-500" />
    <div className="flex-1">
      <p className="pointer-events-none font-medium text-sm text-white">
        Incoming Call
      </p>
      <p className="pointer-events-none text-white text-xs opacity-70">
        Guillermo Rauch
      </p>
    </div>
    <div className="h-2 w-2 animate-pulse rounded-full bg-green-500" />
  </div>
);

// Timer Component
const DefaultTimer = () => {
  const [time, setTime] = useState(60);

  useMemo(() => {
    const timer = setInterval(() => {
      setTime((t) => (t > 0 ? t - 1 : 0));
    }, TIMER_INTERVAL_MS);
    return () => clearInterval(timer);
  }, []);

  return (
    <div className="flex w-64 items-center gap-3 overflow-hidden px-4 py-2 text-white">
      <TimerIcon className="h-5 w-5 text-amber-500" />
      <div className="flex-1">
        <p className="pointer-events-none font-medium text-sm text-white">
          {time}s remaining
        </p>
      </div>
      <div className="h-1 w-24 overflow-hidden rounded-full bg-white/20">
        <motion.div
          animate={{ width: "0%" }}
          className="h-full bg-amber-500"
          initial={{ width: "100%" }}
          transition={{ duration: time, ease: "linear" }}
        />
      </div>
    </div>
  );
};

// Notification Component
const Notification = () => (
  <div className="flex w-64 items-center gap-3 overflow-hidden px-4 py-2 text-white">
    <Bell className="h-5 w-5 text-yellow-400" />
    <div className="flex-1">
      <p className="pointer-events-none font-medium text-sm text-white">
        New Message
      </p>
      <p className="pointer-events-none text-white text-xs opacity-70">
        You have a new notification!
      </p>
    </div>
    <span className="rounded-full bg-yellow-400/40 px-2 py-0.5 text-xs text-yellow-500">
      1
    </span>
  </div>
);

// Music Player Component
const MusicPlayer = () => {
  const [playing, setPlaying] = useState(true);
  return (
    <div className="flex w-72 items-center gap-3 overflow-hidden px-4 py-2 text-white">
      <Music2 className="h-5 w-5 text-pink-500" />
      <div className="min-w-0 flex-1">
        <p className="pointer-events-none truncate font-medium text-sm text-white">
          Lofi Chill Beats
        </p>
        <p className="pointer-events-none truncate text-white text-xs opacity-70">
          DJ Smooth
        </p>
      </div>
      <button
        className="rounded-full p-1 hover:bg-white/30"
        onClick={() => setPlaying(false)}
        type="button"
      >
        <SkipBack className="h-4 w-4 text-white" />
      </button>
      <button
        className="rounded-full p-1 hover:bg-white/30"
        onClick={() => setPlaying((p) => !p)}
        type="button"
      >
        {playing ? (
          <Pause className="h-4 w-4 text-white" />
        ) : (
          <Play className="h-4 w-4 text-white" />
        )}
      </button>
      <button
        className="rounded-full p-1 hover:bg-white/30"
        onClick={() => setPlaying(true)}
        type="button"
      >
        <SkipForward className="h-4 w-4 text-white" />
      </button>
    </div>
  );
};

type View = "idle" | "ring" | "timer" | "notification" | "music";

export interface DynamicIslandProps {
  className?: string;
  idleContent?: ReactNode;
  onViewChange?: (view: View) => void;
  ringContent?: ReactNode;
  timerContent?: ReactNode;
  view?: View;
}

export default function DynamicIsland({
  view: controlledView,
  onViewChange,
  idleContent,
  ringContent,
  timerContent,
  className = "",
}: DynamicIslandProps) {
  const [internalView, setInternalView] = useState<View>("idle");
  const [variantKey, setVariantKey] = useState<string>("idle");
  const shouldReduceMotion = useReducedMotion();

  const view = controlledView ?? internalView;

  const content = useMemo(() => {
    switch (view) {
      case "ring":
        return ringContent ?? <DefaultRing />;
      case "timer":
        return timerContent ?? <DefaultTimer />;
      case "notification":
        return <Notification />;
      case "music":
        return <MusicPlayer />;
      default:
        return idleContent ?? <DefaultIdle />;
    }
  }, [view, idleContent, ringContent, timerContent]);

  const handleViewChange = (newView: View) => {
    if (view === newView) {
      return;
    }
    setVariantKey(`${view}-${newView}`);
    if (onViewChange) {
      onViewChange(newView);
    } else {
      setInternalView(newView);
    }
  };

  return (
    <div className={`h-[200px] ${className}`}>
      <div className="relative flex h-full w-full flex-col justify-center">
        <motion.div
          className="mx-auto w-fit min-w-[100px] overflow-hidden rounded-full bg-black"
          layout
          style={{ borderRadius: 32 }}
          transition={
            shouldReduceMotion
              ? { duration: 0 }
              : {
                  bounce:
                    BOUNCE_VARIANTS[
                      variantKey as keyof typeof BOUNCE_VARIANTS
                    ] ?? DEFAULT_BOUNCE,
                  duration: 0.25,
                  type: "spring" as const,
                }
          }
        >
          <motion.div
            animate={
              shouldReduceMotion
                ? { opacity: 1, scale: 1 }
                : {
                    filter: "blur(0px)",
                    opacity: 1,
                    originX: 0.5,
                    originY: 0.5,
                    scale: 1,
                    transition: { delay: 0.05 },
                  }
            }
            initial={{
              filter: "blur(5px)",
              opacity: 0,
              originX: 0.5,
              originY: 0.5,
              scale: 0.9,
            }}
            key={view}
            transition={{
              bounce:
                BOUNCE_VARIANTS[variantKey as keyof typeof BOUNCE_VARIANTS] ??
                DEFAULT_BOUNCE,
              type: "spring" as const,
            }}
          >
            {content}
          </motion.div>
        </motion.div>

        <div className="absolute bottom-2 left-1/2 z-10 flex -translate-x-1/2 justify-center gap-1 rounded-full border bg-background p-1">
          {[
            { icon: <CloudLightning className="size-3" />, key: "idle" },
            { icon: <Phone className="size-3" />, key: "ring" },
            { icon: <TimerIcon className="size-3" />, key: "timer" },
            { icon: <Bell className="size-3" />, key: "notification" },
            { icon: <Music2 className="size-3" />, key: "music" },
          ].map(({ key, icon }) => (
            <button
              aria-label={key}
              className="flex size-8 cursor-pointer items-center justify-center rounded-full border bg-primary px-2"
              key={key}
              onClick={() => {
                if (view !== key) {
                  setVariantKey(`${view}-${key}`);
                  handleViewChange(key as View);
                }
              }}
              type="button"
            >
              {icon}
            </button>
          ))}
        </div>
      </div>
    </div>
  );
}

Similar components