Bootstrap 5 Floating Draggable Action Menu
A floating action menu that fans out a row of round icon buttons when tapped and can be dragged anywhere on the canvas.
<section class="wdw-stage p-0">
<div class="fab-stage position-relative overflow-hidden w-100" id="fabStage">
<div class="fab-bar d-flex align-items-center gap-3 px-4" aria-hidden="true">
<span class="wdw-icon"><i class="bi bi-grid-1x2-fill"></i></span>
<span class="sk" style="width:90px"></span>
<span class="sk ms-auto d-none d-sm-block" style="width:60px"></span><span class="sk d-none d-sm-block" style="width:60px"></span><span class="sk" style="width:36px;height:36px;border-radius:50%"></span>
</div>
<div class="fab-copy text-center" id="fabCopy">
<span class="wdw-eyebrow mb-2">Floating action menu</span>
<h2 class="wdw-heading display-6 mb-2">Drag the menu where you want</h2>
<p class="wdw-muted mb-0">Tap the button to fan out actions, or grab it and move it anywhere.</p>
</div>
<div class="fab-cards container" style="max-width:1000px" aria-hidden="true">
<div class="row g-3">
<div class="col-12 col-md-4"><div class="wdw-card-sm p-3"><span class="sk mb-3" style="width:40%"></span><span class="sk mb-2" style="width:90%"></span><span class="sk mb-2" style="width:75%"></span><span class="sk" style="width:55%"></span></div></div>
<div class="col-md-4 d-none d-md-block"><div class="wdw-card-sm p-3"><span class="sk mb-3" style="width:50%"></span><span class="sk mb-2" style="width:85%"></span><span class="sk mb-2" style="width:70%"></span><span class="sk" style="width:60%"></span></div></div>
<div class="col-md-4 d-none d-md-block"><div class="wdw-card-sm p-3"><span class="sk mb-3" style="width:35%"></span><span class="sk mb-2" style="width:92%"></span><span class="sk mb-2" style="width:68%"></span><span class="sk" style="width:48%"></span></div></div>
</div>
</div>
<div class="fab-menu" id="fabMenu" role="toolbar" aria-label="Floating menu">
<div class="fab-tray" aria-hidden="true"></div>
<button type="button" class="fab-item fab-toggle btn btn-primary rounded-circle" aria-label="Toggle menu" aria-expanded="false" aria-controls="fabMenu"><i class="bi bi-list" aria-hidden="true"></i></button>
<button type="button" class="fab-item btn btn-light rounded-circle shadow-sm" aria-label="Profile" data-bs-title="Profile"><i class="bi bi-person" aria-hidden="true"></i></button>
<button type="button" class="fab-item btn btn-light rounded-circle shadow-sm" aria-label="Messages" data-bs-title="Messages"><i class="bi bi-chat-dots" aria-hidden="true"></i></button>
<button type="button" class="fab-item btn btn-light rounded-circle shadow-sm" aria-label="Favourites" data-bs-title="Favourites"><i class="bi bi-heart" aria-hidden="true"></i></button>
<button type="button" class="fab-item btn btn-light rounded-circle shadow-sm" aria-label="Settings" data-bs-title="Settings"><i class="bi bi-gear" aria-hidden="true"></i></button>
</div>
</div>
</section>
@import url("https://cdnjs.cloudflare.com/ajax/libs/bootstrap-icons/1.11.3/font/bootstrap-icons.min.css");
/* Web Designer Wall library style — shared base for every Bootstrap 5 snippet.
Indigo accent, Plus Jakarta Sans, soft cards, pill buttons. Works in light and dark. */
@import url("https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap");
:root,
[data-bs-theme="light"] {
--wdw-font: "Plus Jakarta Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--bs-body-font-family: var(--wdw-font);
--bs-primary: #4f46e5;
--bs-primary-rgb: 79, 70, 229;
--bs-primary-text-emphasis: #3730a3;
--bs-primary-bg-subtle: #eef2ff;
--bs-primary-border-subtle: #c7d2fe;
--bs-link-color: #4f46e5;
--bs-link-color-rgb: 79, 70, 229;
--bs-link-hover-color: #4338ca;
--bs-link-hover-color-rgb: 67, 56, 202;
--bs-focus-ring-color: rgba(79, 70, 229, 0.25);
--bs-border-radius: 0.75rem;
--bs-border-radius-lg: 1rem;
--bs-border-radius-xl: 1.25rem;
--wdw-radius-card: 1.25rem;
--wdw-shadow: 0 1px 2px rgba(16, 24, 40, 0.05), 0 16px 40px -16px rgba(16, 24, 40, 0.22);
--wdw-shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.06), 0 4px 12px -4px rgba(16, 24, 40, 0.12);
--wdw-stage: #f5f6fb;
}
[data-bs-theme="dark"] {
--bs-primary: #818cf8;
--bs-primary-rgb: 129, 140, 248;
--bs-primary-text-emphasis: #c7d2fe;
--bs-primary-bg-subtle: rgba(129, 140, 248, 0.14);
--bs-primary-border-subtle: rgba(129, 140, 248, 0.35);
--bs-link-color: #a5b4fc;
--bs-link-color-rgb: 165, 180, 252;
--bs-link-hover-color: #c7d2fe;
--bs-link-hover-color-rgb: 199, 210, 254;
--bs-focus-ring-color: rgba(129, 140, 248, 0.35);
--wdw-shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 16px 40px -16px rgba(0, 0, 0, 0.7);
--wdw-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4), 0 4px 12px -4px rgba(0, 0, 0, 0.5);
--wdw-stage: #0f1117;
}
body { font-family: var(--wdw-font); -webkit-font-smoothing: antialiased; }
/* Bootstrap compiles its blue into these components; point them at the accent. */
.btn-primary {
--bs-btn-bg: var(--bs-primary); --bs-btn-border-color: var(--bs-primary);
--bs-btn-hover-bg: #4338ca; --bs-btn-hover-border-color: #4338ca;
--bs-btn-active-bg: #3730a3; --bs-btn-active-border-color: #3730a3;
--bs-btn-disabled-bg: var(--bs-primary); --bs-btn-disabled-border-color: var(--bs-primary);
--bs-btn-focus-shadow-rgb: var(--bs-primary-rgb);
}
[data-bs-theme="dark"] .btn-primary { --bs-btn-color: #0f1117; --bs-btn-hover-bg: #a5b4fc; --bs-btn-hover-border-color: #a5b4fc; --bs-btn-hover-color: #0f1117; --bs-btn-active-color: #0f1117; }
.btn-outline-primary {
--bs-btn-color: var(--bs-primary); --bs-btn-border-color: var(--bs-primary);
--bs-btn-hover-bg: var(--bs-primary); --bs-btn-hover-border-color: var(--bs-primary);
--bs-btn-active-bg: var(--bs-primary); --bs-btn-active-border-color: var(--bs-primary);
--bs-btn-focus-shadow-rgb: var(--bs-primary-rgb);
}
.btn { --bs-btn-font-weight: 600; min-height: 44px; display: inline-flex; align-items: center; justify-content: center; gap: .5rem; }
.btn-sm { min-height: 36px; }
.form-control, .form-select { min-height: 44px; border-radius: .75rem; }
.form-control:focus, .form-select:focus, .form-check-input:focus { border-color: rgba(var(--bs-primary-rgb), .6); box-shadow: 0 0 0 .25rem var(--bs-focus-ring-color); }
.form-check-input:checked { background-color: var(--bs-primary); border-color: var(--bs-primary); }
.nav-pills { --bs-nav-pills-link-active-bg: var(--bs-primary); }
.nav { --bs-nav-link-color: var(--bs-secondary-color); --bs-nav-link-hover-color: var(--bs-emphasis-color); }
.pagination { --bs-pagination-active-bg: var(--bs-primary); --bs-pagination-active-border-color: var(--bs-primary); --bs-pagination-color: var(--bs-body-color); --bs-pagination-focus-box-shadow: 0 0 0 .25rem var(--bs-focus-ring-color); }
.progress, .progress-stacked { --bs-progress-bar-bg: var(--bs-primary); }
.list-group { --bs-list-group-active-bg: var(--bs-primary); --bs-list-group-active-border-color: var(--bs-primary); }
.dropdown-menu { --bs-dropdown-link-active-bg: var(--bs-primary); border-radius: .875rem; box-shadow: var(--wdw-shadow-sm); }
.accordion { --bs-accordion-active-bg: var(--bs-primary-bg-subtle); --bs-accordion-active-color: var(--bs-primary-text-emphasis); --bs-accordion-btn-focus-box-shadow: 0 0 0 .25rem var(--bs-focus-ring-color); }
.badge { font-weight: 600; }
/* Library building blocks — use these instead of re-inventing them per snippet. */
.wdw-stage { min-height: 100vh; display: flex; align-items: center; background: var(--wdw-stage); padding-block: 3rem; }
.wdw-card { background: var(--bs-body-bg); border: 1px solid var(--bs-border-color-translucent); border-radius: var(--wdw-radius-card); box-shadow: var(--wdw-shadow); }
.wdw-card-sm { background: var(--bs-body-bg); border: 1px solid var(--bs-border-color-translucent); border-radius: 1rem; box-shadow: var(--wdw-shadow-sm); }
.wdw-aurora { position: relative; color: #fff; background: radial-gradient(60% 55% at 100% 0, #818cf8 0, transparent 70%), radial-gradient(65% 60% at 0 100%, #ec4899 0, transparent 65%), #1e1b4b; }
.wdw-eyebrow { display: inline-block; font-size: .75rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--bs-primary); }
.wdw-heading { font-weight: 800; letter-spacing: -.02em; color: var(--bs-emphasis-color); }
.wdw-muted { color: var(--bs-secondary-color); }
.wdw-icon { width: 2.75rem; height: 2.75rem; flex: none; display: inline-flex; align-items: center; justify-content: center; border-radius: .875rem; background: var(--bs-primary-bg-subtle); color: var(--bs-primary); }
/* Component */
.fab-stage { min-height: 100vh; background: radial-gradient(circle at 1px 1px, var(--bs-border-color) 1px, transparent 0) 0 0/24px 24px, var(--wdw-stage); touch-action: none; }
.fab-bar { position: absolute; inset: 0 0 auto 0; height: 72px; background: var(--bs-body-bg); border-bottom: 1px solid var(--bs-border-color-translucent); }
.sk { display: block; height: 10px; border-radius: 99px; background: var(--bs-secondary-bg); }
.fab-cards { position: absolute; left: 50%; bottom: 1.5rem; transform: translateX(-50%); width: 100%; padding-inline: 1rem; }
.fab-cards .wdw-card-sm { min-height: 150px; }
.fab-copy h2 { text-wrap: balance; }
.fab-copy { position: absolute; left: 50%; top: 120px; transform: translateX(-50%); width: min(30rem, 88%); pointer-events: none; }
.fab-menu { position: absolute; inset: 0; pointer-events: none; }
.fab-tray { position: absolute; height: 88px; margin: -12px 0 0 -12px; border-radius: 999px; background: rgba(var(--bs-body-bg-rgb), .65); border: 1px solid var(--bs-border-color-translucent); box-shadow: var(--wdw-shadow); backdrop-filter: blur(10px); transition: left .45s cubic-bezier(.34,1.56,.64,1), top .45s cubic-bezier(.34,1.56,.64,1), width .45s cubic-bezier(.34,1.56,.64,1); }
.fab-item { position: absolute; width: 64px; height: 64px; padding: 0; font-size: 1.4rem; pointer-events: auto; transition: left .45s cubic-bezier(.34,1.56,.64,1), top .45s cubic-bezier(.34,1.56,.64,1), opacity .2s, visibility 0s, background .15s, color .15s, transform .15s; }
.fab-item:not(.fab-toggle):hover { background: var(--bs-primary-bg-subtle); color: var(--bs-primary); }
.fab-toggle { z-index: 2; cursor: grab; touch-action: none; box-shadow: 0 10px 24px -6px rgba(var(--bs-primary-rgb), .6); }
.fab-menu.dragging .fab-item, .fab-menu.dragging .fab-tray { transition: none; }
.fab-menu.dragging .fab-toggle { cursor: grabbing; transform: scale(1.08); }
.fab-menu:not(.open) .fab-item:not(.fab-toggle) { opacity: 0; visibility: hidden; pointer-events: none; transition: left .45s, top .45s, opacity .2s, visibility 0s .45s; }
@media (prefers-reduced-motion: reduce) { .fab-item, .fab-tray, .fab-menu:not(.open) .fab-item:not(.fab-toggle) { transition: none; } }
function initFloatingMenu() {
const stage = document.getElementById('fabStage');
const menu = document.getElementById('fabMenu');
if (!stage || !menu) return;
const head = menu.querySelector('.fab-toggle');
const items = Array.from(menu.querySelectorAll('.fab-item:not(.fab-toggle)'));
const size = 64, gap = 76;
const copy = document.getElementById('fabCopy');
const tray = menu.querySelector('.fab-tray');
let open = false;
let pos = { x: 0, y: 0 };
function layout() {
const need = items.length * gap + size;
const right = stage.clientWidth - pos.x, left = pos.x + size;
const dir = right >= need || right >= left ? 1 : -1;
head.style.left = pos.x + 'px';
head.style.top = pos.y + 'px';
const span = open ? items.length * gap + size + 24 : size + 24;
tray.style.width = span + 'px';
tray.style.top = pos.y + 'px';
tray.style.left = (open && dir < 0 ? pos.x - items.length * gap : pos.x) + 'px';
tray.style.opacity = open ? 1 : 0;
items.forEach((it, i) => {
it.style.left = (pos.x + (open ? dir * (i + 1) * gap : 0)) + 'px';
it.style.top = pos.y + 'px';
});
}
function setOpen(v) {
open = v;
menu.classList.toggle('open', v);
head.setAttribute('aria-expanded', v);
layout();
}
const fan = items.length * gap + size;
items.forEach((it) => { if (window.bootstrap) new bootstrap.Tooltip(it, { placement: 'bottom', trigger: 'hover focus' }); });
const copyH = copy ? copy.offsetHeight : 0;
const groupH = copyH + 48 + size;
const top = Math.max(88, Math.round((stage.clientHeight - groupH) / 2) - 40);
if (copy) copy.style.top = top + 'px';
pos = { x: Math.max(16, Math.round((stage.clientWidth - fan) / 2)), y: top + copyH + 48 };
layout();
setTimeout(() => setOpen(true), 400);
let sx = 0, sy = 0, origin = null, moved = false;
head.addEventListener('pointerdown', (e) => {
head.setPointerCapture(e.pointerId);
sx = e.clientX; sy = e.clientY; origin = { ...pos }; moved = false;
});
head.addEventListener('pointermove', (e) => {
if (!origin) return;
const dx = e.clientX - sx, dy = e.clientY - sy;
if (!moved && Math.hypot(dx, dy) < 4) return;
if (!moved) { moved = true; menu.classList.add('dragging'); setOpen(false); }
pos.x = Math.min(stage.clientWidth - size, Math.max(0, origin.x + dx));
pos.y = Math.min(stage.clientHeight - size, Math.max(0, origin.y + dy));
layout();
});
const end = () => {
if (!origin) return;
origin = null;
menu.classList.remove('dragging');
if (!moved) setOpen(!open);
};
head.addEventListener('pointerup', end);
head.addEventListener('pointercancel', end);
head.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setOpen(!open); }
});
window.addEventListener('resize', () => {
pos.x = Math.min(pos.x, stage.clientWidth - size);
pos.y = Math.min(pos.y, stage.clientHeight - size);
layout();
});
}
initFloatingMenu();
Built on Bootstrap 5.3.8 with plain JavaScript — no jQuery. Supports light and dark mode via data-bs-theme.