A modern, fully interactive Dynamic Island component built with HTML, Vanilla JavaScript, and Tailwind CSS.
This project replicates the fluid, organic "spring physics" animations found in modern mobile interfaces, adapted for a responsive web environment. It features a control panel to demonstrate various UI states including Media Player, Phone Calls, and Biometric authentication.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Modern Dynamic Island UI</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
/* Custom Spring Animation Curve for that "Bouncy" Apple feel */
.island-transition {
transition: all 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
/* Smooth fade for content */
.content-transition {
transition: opacity 0.3s ease, transform 0.3s ease;
}
/* Sound Wave Animation */
.bar {
animation: wave 1s ease-in-out infinite;
}
.bar:nth-child(2) { animation-delay: 0.1s; }
.bar:nth-child(3) { animation-delay: 0.2s; }
.bar:nth-child(4) { animation-delay: 0.3s; }
.bar:nth-child(5) { animation-delay: 0.4s; }
@keyframes wave {
0%, 100% { height: 10%; }
50% { height: 100%; }
}
/* Face ID Spinner */
@keyframes spin-scan {
0% { transform: rotate(0deg); border-top-color: #3b82f6; }
50% { transform: rotate(180deg); border-top-color: #ec4899; }
100% { transform: rotate(360deg); border-top-color: #3b82f6; }
}
.scan-spinner {
border-top-color: transparent;
animation: spin-scan 1.5s linear infinite;
}
</style>
</head>
<body class="bg-neutral-950 text-white min-h-screen flex flex-col items-center justify-center relative overflow-hidden font-sans selection:bg-blue-500 selection:text-white">
<!-- Background Gradient Blob for aesthetics -->
<div class="absolute top-0 left-0 w-full h-full overflow-hidden -z-10 pointer-events-none">
<div class="absolute top-[-10%] left-[20%] w-[500px] h-[500px] bg-purple-600/20 rounded-full blur-[100px]"></div>
<div class="absolute bottom-[-10%] right-[20%] w-[500px] h-[500px] bg-blue-600/10 rounded-full blur-[100px]"></div>
</div>
<!-- =========================== -->
<!-- THE DYNAMIC ISLAND -->
<!-- =========================== -->
<!--
State Logic:
- Default: w-[120px] h-[35px]
- Expanded (Music): w-[350px] h-[160px]
- Notification (Call): w-[320px] h-[70px]
- Square (FaceID): w-[140px] h-[140px]
-->
<div id="island"
class="fixed top-6 left-1/2 -translate-x-1/2 bg-black shadow-2xl shadow-black/50 z-50 overflow-hidden cursor-pointer island-transition group
w-[120px] h-[36px] rounded-[24px] border border-white/5 ring-1 ring-white/5">
<!-- INNER CONTENT WRAPPER -->
<div class="relative w-full h-full">
<!-- 1. IDLE STATE (Camera & Sensors) -->
<div id="view-idle" class="absolute inset-0 flex items-center justify-between px-3 w-full h-full content-transition">
<!-- Left Sensor -->
<div class="w-2 h-2 rounded-full bg-neutral-800/80"></div>
<!-- Right Camera Hole Simulation -->
<div class="w-3 h-3 rounded-full bg-neutral-900/50 shadow-inner"></div>
</div>
<!-- 2. MUSIC PLAYER STATE -->
<div id="view-music" class="absolute inset-0 opacity-0 scale-90 pointer-events-none content-transition flex flex-col p-5">
<!-- Top Row: Album & Wave -->
<div class="flex items-center justify-between mb-3">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-yellow-400 to-red-600 shadow-lg flex items-center justify-center text-[10px] font-bold text-black">
<svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 24 24"><path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/></svg>
</div>
<div class="flex flex-col">
<span class="text-xs font-bold text-gray-200">Starboy</span>
<span class="text-[10px] text-gray-500">The Weeknd</span>
</div>
</div>
<!-- Animated Waveform -->
<div class="flex items-end gap-[3px] h-4">
<div class="bar w-1 bg-green-400 rounded-full h-full"></div>
<div class="bar w-1 bg-green-400 rounded-full h-full"></div>
<div class="bar w-1 bg-green-400 rounded-full h-full"></div>
<div class="bar w-1 bg-green-400 rounded-full h-full"></div>
<div class="bar w-1 bg-green-400 rounded-full h-full"></div>
</div>
</div>
<!-- Progress Bar -->
<div class="w-full h-1.5 bg-neutral-800 rounded-full mb-4 overflow-hidden">
<div class="h-full w-2/3 bg-white rounded-full"></div>
</div>
<!-- Controls -->
<div class="flex items-center justify-center gap-8 text-white">
<button class="hover:text-gray-300 transition"><svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24"><path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/></svg></button>
<button class="hover:scale-110 transition"><svg class="w-8 h-8" fill="currentColor" viewBox="0 0 24 24"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg></button>
<button class="hover:text-gray-300 transition"><svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24"><path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/></svg></button>
</div>
</div>
<!-- 3. PHONE CALL STATE -->
<div id="view-call" class="absolute inset-0 opacity-0 scale-90 pointer-events-none content-transition flex items-center justify-between px-4 w-full h-full">
<div class="flex items-center gap-3">
<img src="https://ui-avatars.com/api/?name=Sarah+J&background=random" class="w-10 h-10 rounded-full border-2 border-neutral-800" alt="Avatar">
<div class="flex flex-col">
<span class="text-xs text-gray-400">iPhone</span>
<span class="text-sm font-semibold">Sarah Jenkins</span>
</div>
</div>
<div class="flex items-center gap-3">
<button class="w-10 h-10 rounded-full bg-red-500/20 text-red-500 flex items-center justify-center hover:bg-red-500 hover:text-white transition">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 9c-1.6 0-3.15.25-4.6.72l-1.2-1.2c1.8-.72 3.74-1.14 5.8-1.14 2.06 0 4 .42 5.8 1.14l-1.2 1.2c-1.45-.47-3-.72-4.6-.72zm0-4c-2.6 0-5.07.56-7.3 1.58L3.5 5.37C6.07 4.15 8.94 3.5 12 3.5s5.93.65 8.5 1.87l-1.2 1.21C17.07 5.56 14.6 5 12 5zm-8 12.5c0 1.66 1.34 3 3 3h10c1.66 0 3-1.34 3-3V16l-3.34-3.34-1.66 1.66V12h-4v2.32l-1.66-1.66L4 16v1.5z"/></svg>
</button>
<button class="w-10 h-10 rounded-full bg-green-500 text-white flex items-center justify-center hover:bg-green-400 transition">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M20.01 15.38c-1.23 0-2.42-.2-3.53-.56-.35-.12-.74-.03-1.01.24l-1.57 1.97c-2.83-1.44-5.15-3.75-6.59-6.59l1.97-1.57c.26-.27.35-.66.24-1.01-.37-1.11-.56-2.3-.56-3.53 0-.54-.45-.99-.99-.99H4.19C3.65 3.3 3 3.24 3 3.99 3 13.28 10.73 21 20.01 21c.71 0 .99-.63.99-1.18v-3.45c0-.54-.45-.99-.99-.99z"/></svg>
</button>
</div>
</div>
<!-- 4. FACE ID STATE -->
<div id="view-faceid" class="absolute inset-0 opacity-0 scale-90 pointer-events-none content-transition flex flex-col items-center justify-center w-full h-full">
<div class="w-12 h-12 rounded-full border-4 border-neutral-700 scan-spinner mb-3"></div>
<span class="text-xs font-semibold text-blue-400 tracking-wider">FACE ID</span>
</div>
</div>
</div>
<!-- =========================== -->
<!-- DEMO CONTROL PANEL -->
<!-- =========================== -->
<main class="text-center mt-32 z-10">
<h1 class="text-4xl font-bold mb-4 bg-clip-text text-transparent bg-gradient-to-r from-white to-gray-500">
Tailwind Dynamic Island
</h1>
<p class="text-gray-400 mb-10">Interact with the buttons below to trigger states.</p>
<div class="grid grid-cols-2 gap-4 max-w-md mx-auto px-4">
<button onclick="setIslandState('idle')"
class="px-6 py-3 rounded-xl bg-neutral-900 border border-neutral-800 hover:bg-neutral-800 hover:border-neutral-600 transition flex items-center justify-center gap-2">
<span>βͺ</span> Reset / Idle
</button>
<button onclick="setIslandState('music')"
class="px-6 py-3 rounded-xl bg-neutral-900 border border-neutral-800 hover:bg-neutral-800 hover:border-green-500/50 transition flex items-center justify-center gap-2">
<span>π΅</span> Play Music
</button>
<button onclick="setIslandState('call')"
class="px-6 py-3 rounded-xl bg-neutral-900 border border-neutral-800 hover:bg-neutral-800 hover:border-blue-500/50 transition flex items-center justify-center gap-2">
<span>π</span> Incoming Call
</button>
<button onclick="setIslandState('faceid')"
class="px-6 py-3 rounded-xl bg-neutral-900 border border-neutral-800 hover:bg-neutral-800 hover:border-pink-500/50 transition flex items-center justify-center gap-2">
<span>π</span> Face ID
</button>
</div>
<p class="mt-8 text-xs text-neutral-600">Tip: Click the Island itself to toggle collapse.</p>
</main>
<!-- =========================== -->
<!-- JAVASCRIPT LOGIC -->
<!-- =========================== -->
<script>
const island = document.getElementById('island');
// Views
const views = {
idle: document.getElementById('view-idle'),
music: document.getElementById('view-music'),
call: document.getElementById('view-call'),
faceid: document.getElementById('view-faceid')
};
let currentState = 'idle';
// Function to hide all views
function hideAllViews() {
Object.values(views).forEach(view => {
view.classList.add('opacity-0', 'scale-90', 'pointer-events-none');
view.classList.remove('opacity-100', 'scale-100', 'pointer-events-auto');
});
}
// Function to show specific view
function showView(viewId) {
hideAllViews();
setTimeout(() => {
const view = views[viewId];
if(view) {
view.classList.remove('opacity-0', 'scale-90', 'pointer-events-none');
view.classList.add('opacity-100', 'scale-100', 'pointer-events-auto');
}
}, 200); // Slight delay for the container to resize first
}
// Main State Controller
function setIslandState(state) {
currentState = state;
// Reset Styles
island.className = `fixed top-6 left-1/2 -translate-x-1/2 bg-black shadow-2xl shadow-black/50 z-50 overflow-hidden cursor-pointer island-transition group border border-white/10 ring-1 ring-white/5`;
switch(state) {
case 'idle':
// Pill Shape
island.classList.add('w-[120px]', 'h-[36px]', 'rounded-[24px]');
showView('idle');
break;
case 'music':
// Large Rect
island.classList.add('w-[350px]', 'h-[160px]', 'rounded-[32px]');
showView('music');
break;
case 'call':
// Wide Pill
island.classList.add('w-[320px]', 'h-[70px]', 'rounded-[35px]');
showView('call');
break;
case 'faceid':
// Square
island.classList.add('w-[140px]', 'h-[140px]', 'rounded-[32px]');
showView('faceid');
// Auto reset faceid after 2 seconds
setTimeout(() => {
if(currentState === 'faceid') setIslandState('idle');
}, 2500);
break;
}
}
// Toggle on click (Interactive feature)
island.addEventListener('click', () => {
if (currentState === 'idle') {
setIslandState('music');
} else {
setIslandState('idle');
}
});
// Initialize
setIslandState('idle');
</script>
</body>
</html>