Docs
Go back
Component by
OV
OblivionTSX
dynamic-island
"use client"
import React, { useState } from "react"
import { motion, AnimatePresence } from "framer-motion"
import { cn } from "@/lib/utils"
// Ensure lucide-react is installed for these icons
import { Phone, Zap, Check } from "lucide-react"
export const componentMeta = {
name: "Dynamic Island",
description: "An interactive, iOS-inspired dynamic island component that morphs fluidly between immersive states.",
props: {
className: { type: "string", description: "Additional CSS classes", default: '""' },
}
}
type IslandState = "idle" | "music" | "call" | "notification" | "battery"
export default function DynamicIsland({ className }: { className?: string }) {
const [state, setState] = useState<IslandState>("idle")
const ISLAND_VARIANTS = {
idle: { width: 120, height: 40, borderRadius: 20 },
music: { width: 340, height: 80, borderRadius: 40 },
call: { width: 320, height: 72, borderRadius: 36 },
notification: { width: 300, height: 72, borderRadius: 36 },
battery: { width: 280, height: 50, borderRadius: 25 }
}
return (
<div className={cn("flex flex-col items-center justify-center p-8 gap-12 w-full", className)}>
{/* The Island */}
<div className="relative flex justify-center w-full min-h-[120px]">
<motion.div
layout
variants={ISLAND_VARIANTS}
initial="idle"
animate={state}
transition={{ type: "spring", stiffness: 400, damping: 30 }}
className="bg-black text-white overflow-hidden shadow-2xl relative flex items-center justify-center cursor-pointer"
onClick={() => setState("idle")}
>
<AnimatePresence mode="popLayout">
{/* IDLE STATE */}
{state === "idle" && (
<motion.div
key="idle"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0 }}
className="w-full h-full flex items-center justify-center gap-4"
>
{/* Camera / Sensor cutouts mimicking the design */}
<div className="w-[14px] h-[14px] rounded-full bg-[#111111] shadow-[inset_0px_1px_2px_rgba(255,255,255,0.05)]" />
<div className="w-8 h-[14px] rounded-full bg-[#111111] shadow-[inset_0px_1px_2px_rgba(255,255,255,0.05)]" />
</motion.div>
)}
{/* MUSIC (SPOTIFY) STATE */}
{state === "music" && (
<motion.div
key="music"
initial={{ opacity: 0, filter: "blur(4px)" }}
animate={{ opacity: 1, filter: "blur(0px)" }}
exit={{ opacity: 0, filter: "blur(4px)" }}
transition={{ duration: 0.2 }}
className="w-full h-full flex items-center justify-between px-6 gap-4"
>
<svg viewBox="0 0 24 24" fill="#1DB954" className="w-12 h-12 flex-shrink-0 drop-shadow-sm">
<path d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2zm4.586 14.424c-.18.295-.573.398-.868.217-2.38-1.455-5.372-1.785-8.9-1.002-.335.074-.664-.136-.738-.47-.074-.335.136-.665.47-.738 3.864-.856 7.155-.48 9.82 1.15.295.18.397.573.216.868zm1.2-2.738c-.226.368-.71.49-1.077.264-2.718-1.67-6.867-2.15-9.62-1.18-.415.147-.866-.07-1.012-.486-.147-.414.07-.865.485-1.012 3.197-1.127 7.807-.59 10.96 1.344.368.225.49.71.264 1.076zm.106-2.88c-3.23-1.92-8.544-2.096-11.616-1.16-.487.148-.99-.126-1.137-.613-.148-.487.125-.99.613-1.137 3.54-1.075 9.42-.88 13.14 1.336.438.26.58.85.32 1.288-.26.437-.85.58-1.288.32z"/>
</svg>
<div className="flex-1 flex flex-col justify-center text-left">
<span className="text-[15px] font-bold truncate leading-tight tracking-[0.01em]">Spotify</span>
<span className="text-[12px] text-zinc-400 truncate mt-0.5">Playing Music...</span>
</div>
<div className="flex items-center gap-1.5 h-6">
<motion.div animate={{ height: [12, 24, 12] }} transition={{ repeat: Infinity, duration: 0.6 }} className="w-1 bg-[#1DB954] rounded-full" />
<motion.div animate={{ height: [16, 8, 16] }} transition={{ repeat: Infinity, duration: 0.5 }} className="w-1 bg-[#1DB954] rounded-full" />
<motion.div animate={{ height: [8, 20, 8] }} transition={{ repeat: Infinity, duration: 0.7 }} className="w-1 bg-[#1DB954] rounded-full" />
</div>
</motion.div>
)}
{/* CALL STATE */}
{state === "call" && (
<motion.div
key="call"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="w-full h-full flex items-center justify-between px-3"
>
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-indigo-400 to-purple-600 flex items-center justify-center shadow-inner overflow-hidden relative">
{/* Abstract Avatar Graphic */}
<div className="absolute bottom-0 w-8 h-5 bg-white/20 rounded-t-full" />
<div className="absolute top-2 w-5 h-5 bg-white/20 rounded-full" />
</div>
<div className="flex flex-col text-left">
<span className="text-[15px] font-semibold text-white leading-tight">Mom</span>
<span className="text-[12px] text-zinc-400">Incoming call...</span>
</div>
</div>
<div className="flex gap-2">
<motion.button whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} className="w-11 h-11 rounded-full bg-[#FF3B30] flex items-center justify-center shadow-lg">
<Phone size={20} fill="white" className="text-white rotate-[135deg]" />
</motion.button>
<motion.button
animate={{
boxShadow: [
"0px 0px 0px 0px rgba(52,199,89,0.8)",
"0px 0px 0px 8px rgba(52,199,89,0)",
]
}}
transition={{ repeat: Infinity, duration: 1.5 }}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="w-11 h-11 rounded-full bg-[#34C759] flex items-center justify-center shadow-lg"
>
<Phone size={20} fill="white" className="text-white" />
</motion.button>
</div>
</motion.div>
)}
{/* NOTIFICATION STATE (APPLE PAY) */}
{state === "notification" && (
<motion.div
key="notification"
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0 }}
className="w-full h-full flex items-center justify-between px-4"
>
<div className="flex items-center gap-3">
<div className="w-11 h-11 bg-zinc-900 border border-zinc-800 rounded-full flex items-center justify-center shadow-md">
<span className="text-2xl text-white"></span>
</div>
<div className="flex flex-col text-left">
<span className="text-[14px] font-bold text-white tracking-wide leading-tight">Apple Pay</span>
<span className="text-[12px] text-zinc-300 font-medium">$120.00 Sent</span>
</div>
</div>
<div className="min-w-[32px] min-h-[32px] rounded-full flex items-center justify-center bg-zinc-800">
<motion.div initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ type: "spring", stiffness: 500, damping: 25, delay: 0.2 }}>
<Check size={18} strokeWidth={3} className="text-[#34C759]" />
</motion.div>
</div>
</motion.div>
)}
{/* BATTERY STATE */}
{state === "battery" && (
<motion.div
key="battery"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
className="w-full h-full flex items-center justify-between px-5"
>
<div className="flex items-center gap-2">
<Zap size={18} className="text-[#34C759] fill-[#34C759]" />
<span className="text-[14px] font-semibold text-white">Charging</span>
</div>
<div className="flex items-center gap-2">
<span className="text-[14px] font-bold text-[#34C759] mr-1">84%</span>
{/* Detailed Boxy Battery Graphic */}
<div className="relative w-9 h-[18px] rounded-[4px] border border-zinc-500 flex items-center p-[2px]">
<motion.div
initial={{ width: "20%" }}
animate={{ width: "84%" }}
transition={{ duration: 1.5, ease: "easeOut", delay: 0.1 }}
className="h-full bg-[#34C759] rounded-[2px]"
/>
<div className="absolute -right-[3px] top-1/2 -translate-y-1/2 w-[2px] h-[6px] bg-zinc-500 rounded-r-sm" />
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
</div>
{/* Controls */}
<div className="flex flex-wrap gap-3 justify-center items-center p-4 bg-zinc-100 dark:bg-zinc-900 rounded-2xl border border-zinc-200 dark:border-zinc-800">
{(["idle", "music", "call", "notification", "battery"] as const).map((s) => (
<button
key={s}
onClick={() => setState(s)}
className={cn(
"px-4 py-2 text-xs font-semibold rounded-lg capitalize transition-colors duration-200",
state === s
? "bg-zinc-900 text-white dark:bg-white dark:text-zinc-900"
: "bg-white text-zinc-600 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-400 dark:hover:bg-zinc-700 shadow-sm border border-zinc-200 dark:border-zinc-700"
)}
>
{s}
</button>
))}
</div>
</div>
)
}"use client"
import React, { useState } from "react"
import { motion, AnimatePresence } from "framer-motion"
import { cn } from "@/lib/utils"
// Ensure lucide-react is installed for these icons
import { Phone, Zap, Check } from "lucide-react"
export const componentMeta = {
name: "Dynamic Island",
description: "An interactive, iOS-inspired dynamic island component that morphs fluidly between immersive states.",
props: {
className: { type: "string", description: "Additional CSS classes", default: '""' },
}
}
type IslandState = "idle" | "music" | "call" | "notification" | "battery"
export default function DynamicIsland({ className }: { className?: string }) {
const [state, setState] = useState<IslandState>("idle")
const ISLAND_VARIANTS = {
idle: { width: 120, height: 40, borderRadius: 20 },
music: { width: 340, height: 80, borderRadius: 40 },
call: { width: 320, height: 72, borderRadius: 36 },
notification: { width: 300, height: 72, borderRadius: 36 },
battery: { width: 280, height: 50, borderRadius: 25 }
}
return (
<div className={cn("flex flex-col items-center justify-center p-8 gap-12 w-full", className)}>
{/* The Island */}
<div className="relative flex justify-center w-full min-h-[120px]">
<motion.div
layout
variants={ISLAND_VARIANTS}
initial="idle"
animate={state}
transition={{ type: "spring", stiffness: 400, damping: 30 }}
className="bg-black text-white overflow-hidden shadow-2xl relative flex items-center justify-center cursor-pointer"
onClick={() => setState("idle")}
>
<AnimatePresence mode="popLayout">
{/* IDLE STATE */}
{state === "idle" && (
<motion.div
key="idle"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0 }}
className="w-full h-full flex items-center justify-center gap-4"
>
{/* Camera / Sensor cutouts mimicking the design */}
<div className="w-[14px] h-[14px] rounded-full bg-[#111111] shadow-[inset_0px_1px_2px_rgba(255,255,255,0.05)]" />
<div className="w-8 h-[14px] rounded-full bg-[#111111] shadow-[inset_0px_1px_2px_rgba(255,255,255,0.05)]" />
</motion.div>
)}
{/* MUSIC (SPOTIFY) STATE */}
{state === "music" && (
<motion.div
key="music"
initial={{ opacity: 0, filter: "blur(4px)" }}
animate={{ opacity: 1, filter: "blur(0px)" }}
exit={{ opacity: 0, filter: "blur(4px)" }}
transition={{ duration: 0.2 }}
className="w-full h-full flex items-center justify-between px-6 gap-4"
>
<svg viewBox="0 0 24 24" fill="#1DB954" className="w-12 h-12 flex-shrink-0 drop-shadow-sm">
<path d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2zm4.586 14.424c-.18.295-.573.398-.868.217-2.38-1.455-5.372-1.785-8.9-1.002-.335.074-.664-.136-.738-.47-.074-.335.136-.665.47-.738 3.864-.856 7.155-.48 9.82 1.15.295.18.397.573.216.868zm1.2-2.738c-.226.368-.71.49-1.077.264-2.718-1.67-6.867-2.15-9.62-1.18-.415.147-.866-.07-1.012-.486-.147-.414.07-.865.485-1.012 3.197-1.127 7.807-.59 10.96 1.344.368.225.49.71.264 1.076zm.106-2.88c-3.23-1.92-8.544-2.096-11.616-1.16-.487.148-.99-.126-1.137-.613-.148-.487.125-.99.613-1.137 3.54-1.075 9.42-.88 13.14 1.336.438.26.58.85.32 1.288-.26.437-.85.58-1.288.32z"/>
</svg>
<div className="flex-1 flex flex-col justify-center text-left">
<span className="text-[15px] font-bold truncate leading-tight tracking-[0.01em]">Spotify</span>
<span className="text-[12px] text-zinc-400 truncate mt-0.5">Playing Music...</span>
</div>
<div className="flex items-center gap-1.5 h-6">
<motion.div animate={{ height: [12, 24, 12] }} transition={{ repeat: Infinity, duration: 0.6 }} className="w-1 bg-[#1DB954] rounded-full" />
<motion.div animate={{ height: [16, 8, 16] }} transition={{ repeat: Infinity, duration: 0.5 }} className="w-1 bg-[#1DB954] rounded-full" />
<motion.div animate={{ height: [8, 20, 8] }} transition={{ repeat: Infinity, duration: 0.7 }} className="w-1 bg-[#1DB954] rounded-full" />
</div>
</motion.div>
)}
{/* CALL STATE */}
{state === "call" && (
<motion.div
key="call"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="w-full h-full flex items-center justify-between px-3"
>
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-indigo-400 to-purple-600 flex items-center justify-center shadow-inner overflow-hidden relative">
{/* Abstract Avatar Graphic */}
<div className="absolute bottom-0 w-8 h-5 bg-white/20 rounded-t-full" />
<div className="absolute top-2 w-5 h-5 bg-white/20 rounded-full" />
</div>
<div className="flex flex-col text-left">
<span className="text-[15px] font-semibold text-white leading-tight">Mom</span>
<span className="text-[12px] text-zinc-400">Incoming call...</span>
</div>
</div>
<div className="flex gap-2">
<motion.button whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} className="w-11 h-11 rounded-full bg-[#FF3B30] flex items-center justify-center shadow-lg">
<Phone size={20} fill="white" className="text-white rotate-[135deg]" />
</motion.button>
<motion.button
animate={{
boxShadow: [
"0px 0px 0px 0px rgba(52,199,89,0.8)",
"0px 0px 0px 8px rgba(52,199,89,0)",
]
}}
transition={{ repeat: Infinity, duration: 1.5 }}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="w-11 h-11 rounded-full bg-[#34C759] flex items-center justify-center shadow-lg"
>
<Phone size={20} fill="white" className="text-white" />
</motion.button>
</div>
</motion.div>
)}
{/* NOTIFICATION STATE (APPLE PAY) */}
{state === "notification" && (
<motion.div
key="notification"
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0 }}
className="w-full h-full flex items-center justify-between px-4"
>
<div className="flex items-center gap-3">
<div className="w-11 h-11 bg-zinc-900 border border-zinc-800 rounded-full flex items-center justify-center shadow-md">
<span className="text-2xl text-white"></span>
</div>
<div className="flex flex-col text-left">
<span className="text-[14px] font-bold text-white tracking-wide leading-tight">Apple Pay</span>
<span className="text-[12px] text-zinc-300 font-medium">$120.00 Sent</span>
</div>
</div>
<div className="min-w-[32px] min-h-[32px] rounded-full flex items-center justify-center bg-zinc-800">
<motion.div initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ type: "spring", stiffness: 500, damping: 25, delay: 0.2 }}>
<Check size={18} strokeWidth={3} className="text-[#34C759]" />
</motion.div>
</div>
</motion.div>
)}
{/* BATTERY STATE */}
{state === "battery" && (
<motion.div
key="battery"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
className="w-full h-full flex items-center justify-between px-5"
>
<div className="flex items-center gap-2">
<Zap size={18} className="text-[#34C759] fill-[#34C759]" />
<span className="text-[14px] font-semibold text-white">Charging</span>
</div>
<div className="flex items-center gap-2">
<span className="text-[14px] font-bold text-[#34C759] mr-1">84%</span>
{/* Detailed Boxy Battery Graphic */}
<div className="relative w-9 h-[18px] rounded-[4px] border border-zinc-500 flex items-center p-[2px]">
<motion.div
initial={{ width: "20%" }}
animate={{ width: "84%" }}
transition={{ duration: 1.5, ease: "easeOut", delay: 0.1 }}
className="h-full bg-[#34C759] rounded-[2px]"
/>
<div className="absolute -right-[3px] top-1/2 -translate-y-1/2 w-[2px] h-[6px] bg-zinc-500 rounded-r-sm" />
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
</div>
{/* Controls */}
<div className="flex flex-wrap gap-3 justify-center items-center p-4 bg-zinc-100 dark:bg-zinc-900 rounded-2xl border border-zinc-200 dark:border-zinc-800">
{(["idle", "music", "call", "notification", "battery"] as const).map((s) => (
<button
key={s}
onClick={() => setState(s)}
className={cn(
"px-4 py-2 text-xs font-semibold rounded-lg capitalize transition-colors duration-200",
state === s
? "bg-zinc-900 text-white dark:bg-white dark:text-zinc-900"
: "bg-white text-zinc-600 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-400 dark:hover:bg-zinc-700 shadow-sm border border-zinc-200 dark:border-zinc-700"
)}
>
{s}
</button>
))}
</div>
</div>
)
}Save to favoritesCopy to Figma
React
Dynamic Island
An interactive, iOS-inspired dynamic island component that morphs fluidly between immersive states.
Installation
$ npx @cosmoo/oblivion add dynamic-islandDependencies
npm install framer-motion lucide-reactUsage Example
tsx
import DynamicIsland from "@/components/oblivion/dynamic-island"
export default function Demo() {
return (
<div className="p-10 flex justify-center">
<DynamicIsland />
</div>
)
}import DynamicIsland from "@/components/oblivion/dynamic-island"
export default function Demo() {
return (
<div className="p-10 flex justify-center">
<DynamicIsland />
</div>
)
}