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.
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
"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>
);
}