<div class="island-wrapper">
<!-- The Pill Component -->
<div class="island" id="dynamic-island">
<!-- State 1: Loading -->
<div class="island-layer state-loading">
<div class="spinner"></div>
<span class="label">Syncing...</span>
</div>
<!-- State 2: Success -->
<div class="island-layer state-success">
<div class="icon-success">
<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="3" fill="none"><polyline points="20 6 9 17 4 12"></polyline></svg>
</div>
<div class="text-group">
<span class="title">Success</span>
<span class="subtitle">Data updated</span>
</div>
</div>
</div>
<!-- Trigger for Demo -->
<button class="trigger-btn" onclick="triggerAction()">Save Changes</button>
</div>
* { box-sizing: border-box; }
body {
font-family: system-ui, "Segoe UI", sans-serif;
background: #f1f5f9;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding-top: 100px;
}
/* Wrapper to position the island (e.g., fixed at top) */
.island-wrapper {
display: flex;
flex-direction: column;
align-items: center;
gap: 60px;
}
.island {
background: #000;
color: white;
border-radius: 50px;
/* Initial State: Tiny Dot or Hidden */
width: 120px;
height: 36px;
display: flex;
justify-content: center;
align-items: center;
position: relative;
overflow: hidden;
box-shadow: 0 4px 15px rgba(0,0,0,0.15);
/* The Spring Animation */
transition: width 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275),
height 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
/* LAYERS: Absolute positioning to overlay states */
.island-layer {
position: absolute;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
opacity: 0;
transform: scale(0.9);
transition: opacity 0.3s ease, transform 0.3s ease;
pointer-events: none;
}
/* STATE STYLES */
/* 1. Loading State */
.island.mode-loading {
width: 140px;
height: 44px;
}
.island.mode-loading .state-loading {
opacity: 1;
transform: scale(1);
}
.spinner {
width: 18px;
height: 18px;
border: 2px solid rgba(255,255,255,0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
/* 2. Success State */
.island.mode-success {
width: 220px;
height: 54px;
background: #000;
}
.island.mode-success .state-success {
opacity: 1;
transform: scale(1);
}
.icon-success {
color: #4ade80; /* Bright Green */
display: flex;
}
.text-group {
display: flex;
flex-direction: column;
line-height: 1.1;
text-align: left;
}
.title { font-weight: 600; font-size: 0.95rem; }
.subtitle { font-size: 0.75rem; color: #9ca3af; }
/* 3. Idle / Hidden State (Optional: could shrink to 0) */
.island:not(.mode-loading):not(.mode-success) {
width: 0;
height: 0;
opacity: 0;
}
.trigger-btn {
padding: 12px 24px;
background: #2563eb;
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.trigger-btn:hover { background: #1d4ed8; }
@keyframes spin {
to { transform: rotate(360deg); }
}
const island = document.getElementById("dynamic-island");
function triggerAction() {
// Prevent double click
if (island.classList.contains("mode-loading") || island.classList.contains("mode-success")) return;
// 1. Start Loading
island.classList.add("mode-loading");
// Simulate Network Request (1.5 seconds)
setTimeout(() => {
// 2. Switch to Success
island.classList.remove("mode-loading");
island.classList.add("mode-success");
// 3. Close after delay (2.5 seconds)
setTimeout(() => {
island.classList.remove("mode-success");
}, 2500);
}, 1500);
}