Dynamic Island
The Dynamic Island is an iOS-inspired floating action bar that provides contextual actions and notifications at the bottom of the screen.Overview
Implemented indynamic-island.js, this advanced component provides:
- Multiple presets: Pre-configured layouts for different use cases
- Toast notifications: Temporary message display
- Scroll-triggered visibility: Appears after scrolling threshold
- Responsive states: Pill, expanded, and fullscreen modes
- Template system: Dynamic HTML generation with data binding
- Haptic feedback: Visual feedback on interactions
Component Class
Fromdynamic-island.js:203:
class DynamicIsland {
constructor(initialConfig = {}) {
this.config = initialConfig;
this.container = null;
this.island = null;
this.islandContent = null;
this.centerBtn = null;
this.islandCloseBtn = null;
this.contextBadge = null;
this.lastScroll = 0;
this.scrollTimeout = null;
this.isFullscreen = false;
this.escapeHandler = null;
this.currentPreset = initialConfig.presetName || "default";
this.init();
}
}
Presets
Search + Menu + Cart
Fromdynamic-island.js:238:
search_menu_cart: {
getHtmlStructure(data) {
return `
<div class="island-content">
<button class="island-btn secondary" data-action="menu">
<span class="icon">${data.menu.icon}</span>
<span>${data.menu.name}</span>
</button>
<div class="center-content" data-action="search">
<span class="search-icon">${data.search.icon}</span>
<span>${data.search.name}</span>
</div>
<button class="island-btn accent" data-action="cart">
<span class="icon">${data.cart.icon}</span>
<span>${data.cart.name}</span>
</button>
</div>`;
},
data: {
menu: {
icon: ICONS.hamMenu,
name: "Menú",
function: () => {
if (window.innerWidth <= 768) {
if (typeof toggleMenu === "function") {
toggleMenu();
}
} else {
const firstMenuBtn = document.querySelector("a[data-mega]");
if (firstMenuBtn) {
const mouseEnterEvent = new MouseEvent("mouseenter", {
view: window,
bubbles: true,
cancelable: true,
});
firstMenuBtn.dispatchEvent(mouseEnterEvent);
}
}
},
},
// ... more actions
},
}
Search + To Top
Fromdynamic-island.js:291:
search_totop: {
getHtmlStructure(data) {
return `
<div class="island-content">
<div class="center-content" data-action="search">
<span class="search-icon">${data.search.icon}</span>
<span>${data.search.name}</span>
</div>
<button class="island-btn accent" data-action="totop">
<span class="icon">${data.totop.icon}</span>
<span>${data.totop.name}</span>
</button>
</div>`;
},
data: {
search: {
icon: ICONS.search,
name: "Buscar",
function: SafeActions.openSearch,
},
totop: {
icon: ICONS.totop || "↑",
name: "Volver arriba",
function: SafeActions.totop,
},
},
}
Scroll Detection
Fromdynamic-island.js:710:
setupScrollDetection() {
window.addEventListener("scroll", () => {
if (!this.container || !this.island) return;
// Don't show dynamic island if body is scroll-locked (overlays are open)
if (typeof ScrollLock !== "undefined" && ScrollLock.isLocked()) {
return;
}
const currentScroll = window.pageYOffset;
clearTimeout(this.scrollTimeout);
if (currentScroll > 200) {
this.container.classList.add("visible");
setTimeout(() => {
if (!this.isFullscreen && this.island) {
this.island.classList.remove("pill");
this.island.classList.add("expanded");
if (currentScroll > 300 && currentScroll < 600) {
if (this.contextBadge) {
this.contextBadge.classList.add("show");
setTimeout(
() => this.contextBadge.classList.remove("show"),
2000,
);
}
}
}
}, 300);
this.scrollTimeout = setTimeout(() => {
if (!this.isFullscreen && this.island) {
this.island.classList.remove("expanded");
this.island.classList.add("pill");
}
}, 2000);
} else {
this.container.classList.remove("visible");
if (this.island) {
this.island.classList.remove("expanded");
this.island.classList.add("pill");
}
}
this.lastScroll = currentScroll;
});
}
Toast Notifications
Show Toast
Fromdynamic-island.js:794:
showToast(html, data = {}, type = "3s") {
if (!this.island || !this.islandContent) return;
this.container.classList.add("visible");
this.island.setAttribute("data-status", "toast");
if (data.fullWidth) {
this.island.classList.add("full-width");
}
// Save previous state
if (
!this.previousHtml ||
this.island.getAttribute("data-status") !== "toast"
) {
this.previousHtml = this.islandContent.innerHTML;
this.previousData = this.config.data || {};
}
let toastStructure;
const toastData = { ...data };
if (
data.actions &&
Array.isArray(data.actions) &&
data.actions.length > 0
) {
toastStructure = DynamicIsland.toastTemplates.withActions(
html,
data.actions,
);
toastData.actions = data.actions;
} else if (type === "persistent") {
toastStructure = DynamicIsland.toastTemplates.persistent(html);
} else {
toastStructure = DynamicIsland.toastTemplates.simple(html);
}
this.hydrateIsland(toastStructure, toastData);
if (data.actions) {
this._attachToastActionHandlers(data.actions);
}
this._scheduleToastDismiss(type, data.duration);
}
Toast Templates
Fromdynamic-island.js:409:
static toastTemplates = {
simple: (content) => `<div class="island-content">
<div class="toast-content">${content}</div>
</div>`,
persistent: (content) => `<div class="island-content">
<div class="toast-content">
${content}
<button class="toast-close-btn" onclick="closeToast()">×</button>
</div>
</div>`,
withActions: (content, actions) => {
const actionsHtml = actions
.map(
(action, index) =>
`<button class="toast-action-btn" data-action-index="${index}">${action.text}</button>`,
)
.join("");
return `<div class="island-content">
<div class="toast-content">
${content}
<div style="display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 1rem;">
${actionsHtml}
</div>
</div>
</div>`;
},
cookies: (customText) => {
const defaultText =
customText ||
"<small>Este sitio utiliza cookies para mejorar tu experiencia</small>";
return `<div class="island-content">
<div class="toast-content">
<div style="display: flex; align-items: center; gap: 1rem; width: 100%;">
<span style="font-size: 1.8em;">🍪</span>
<div style="flex: 1;">
<strong>Usamos cookies</strong><br>
${defaultText}
</div>
</div>
</div>
</div>`;
},
};
HTML Structure
Fromdynamic-island.html:1:
<div class="dynamic-island-container">
<div
class="dynamic-island pill"
role="region"
aria-label="Dynamic Island"
data-status="tool-set"
>
<button class="close-btn" aria-label="Cerrar">×</button>
<div class="island-content">
<div
class="center-content"
data-action="search"
style="pointer-events: auto; cursor: pointer"
>
<span class="search-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<use href="#icon-search"></use>
</svg>
</span>
<span>Buscar</span>
</div>
<button class="island-btn accent" data-action="totop">
<span class="icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<use href="#arrow-up"></use>
</svg>
</span>
<span>Volver arriba</span>
</button>
</div>
</div>
</div>
Styling
Container and Base Styles
Fromdynamic-island.css:4:
.dynamic-island-container {
position: fixed;
bottom: -100px;
left: 50%;
transform: translateX(-50%);
z-index: 9999;
transition: bottom 0.6s cubic-bezier(0.25, 0.1, 0, 1.02);
width: max-content;
}
.dynamic-island-container.visible {
bottom: 20px;
}
.dynamic-island {
position: relative;
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(40px) saturate(180%);
-webkit-backdrop-filter: blur(40px) saturate(180%);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 30px;
padding: 8px 12px;
display: flex;
align-items: center;
gap: 8px;
box-shadow:
0 10px 40px rgba(0, 0, 0, 0.12),
0 2px 8px rgba(0, 0, 0, 0.08),
inset 0 1px 0 rgba(255, 255, 255, 0.5);
transition: all 0.5s cubic-bezier(0.25, 0.1, 0, 1.02);
max-width: 90vw;
}
State Classes
Fromdynamic-island.css:38:
/* Pill State (default) */
.dynamic-island.pill {
padding: 6px 8px;
gap: 6px;
}
.dynamic-island.pill .island-btn {
width: 40px;
height: 40px;
padding: 0;
}
.dynamic-island.pill .island-btn:not(.center-content) span:not(.icon) {
display: none;
}
/* Expanded State */
.dynamic-island.expanded {
padding: 10px 14px;
gap: 10px;
}
.dynamic-island.expanded .island-btn {
width: auto;
padding: 0 16px;
}
.dynamic-island.expanded .island-btn span:not(.icon) {
display: inline-block !important;
margin-left: 8px;
}
Public API
Fromdynamic-island.js:1212:
window.Klef.DynamicIsland = {
Class: DynamicIsland,
instance: () => dynamicIslandInstance,
presets: DynamicIsland.presets,
toastTemplates: DynamicIsland.toastTemplates,
init: initDynamicIsland,
set: setDynamicIsland,
hydrate: hydrateIsland,
showToast,
listActions: listIslandActions,
trigger: triggerIslandAction,
loadPreset,
showCookieConsent,
checkAndShowCookieConsent,
initCookieConsentUI,
};
Usage Examples
Initialize with Default Preset
window.Klef.DynamicIsland.init();
Load a Different Preset
window.Klef.DynamicIsland.loadPreset('search_menu_cart');
Show a Toast Notification
window.Klef.DynamicIsland.showToast(
'✅ Changes saved successfully',
{ duration: 3000 },
'3s'
);
Show Toast with Actions
window.Klef.DynamicIsland.showToast(
'New update available',
{
actions: [
{
text: 'Update Now',
onClick: () => window.location.reload()
},
{
text: 'Later',
onClick: () => console.log('Dismissed')
}
]
},
'persistent'
);
Cookie Consent
window.Klef.DynamicIsland.showCookieConsent(
'We use cookies to enhance your experience',
{
acceptText: 'Accept All',
essentialText: 'Essential Only',
onAcceptAll: () => console.log('All cookies accepted'),
onEssentialOnly: () => console.log('Only essential cookies')
}
);
Auto-Initialization
Fromdynamic-island.js:1236:
document.addEventListener("DOMContentLoaded", () => {
let initialized = false;
const initOnScroll = () => {
if (!initialized) {
initialized = true;
initDynamicIsland();
window.removeEventListener("scroll", initOnScroll);
}
};
window.addEventListener("scroll", initOnScroll, { passive: true });
});
Browser Support
Requires:- ES6+ JavaScript (classes, arrow functions, template literals)
- CSS backdrop-filter
- CSS custom properties
- Intersection Observer
- Custom Elements v1

