Skip to content

Instantly share code, notes, and snippets.

@tje3d
Last active December 20, 2025 02:20
Show Gist options
  • Select an option

  • Save tje3d/121d1d6d09f6d126f9b1197ceee2f623 to your computer and use it in GitHub Desktop.

Select an option

Save tje3d/121d1d6d09f6d126f9b1197ceee2f623 to your computer and use it in GitHub Desktop.

🏝️ Dynamic Island UI for Web

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment