UI Components Medium
FAB Speed Dial
Floating action button that expands into a speed dial with labeled sub-actions on click, with backdrop and close-on-outside-click.
Open in Lab
MCP
css vanilla-js
Targets: JS HTML
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: Inter, system-ui, sans-serif;
background: #050910;
color: #f2f6ff;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
/* ── Demo shell ── */
.demo {
width: min(540px, 100%);
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.demo-title {
font-size: 1.5rem;
font-weight: 700;
letter-spacing: -0.02em;
color: #f2f6ff;
}
.demo-sub {
font-size: 0.9rem;
color: #8090b0;
line-height: 1.6;
}
.mock-content {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.mock-card {
height: 72px;
border-radius: 0.75rem;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.07);
animation: shimmer 2s ease-in-out infinite alternate;
}
.mock-card:nth-child(2) {
animation-delay: 0.3s;
opacity: 0.7;
}
.mock-card:nth-child(3) {
animation-delay: 0.6s;
opacity: 0.5;
}
@keyframes shimmer {
from {
background: rgba(255, 255, 255, 0.04);
}
to {
background: rgba(255, 255, 255, 0.07);
}
}
/* ── Backdrop ── */
.fab-backdrop {
position: fixed;
inset: 0;
background: rgba(5, 9, 16, 0.7);
backdrop-filter: blur(2px);
-webkit-backdrop-filter: blur(2px);
z-index: 40;
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease;
}
.fab-backdrop.is-visible {
opacity: 1;
pointer-events: auto;
}
/* ── FAB container ── */
.fab-container {
position: fixed;
bottom: 2rem;
right: 2rem;
z-index: 50;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 0;
}
/* ── Sub-actions ── */
.fab-actions {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 0.75rem;
margin-bottom: 0.75rem;
pointer-events: none;
}
.fab-action {
display: flex;
align-items: center;
gap: 0.75rem;
opacity: 0;
transform: translateY(12px) scale(0.85);
transition: opacity 0.22s ease, transform 0.28s cubic-bezier(0.34, 1.4, 0.64, 1);
transition-delay: 0s;
}
/* Open state — stagger using --i */
.fab-container.is-open .fab-actions {
pointer-events: auto;
}
.fab-container.is-open .fab-action {
opacity: 1;
transform: translateY(0) scale(1);
transition-delay: calc(var(--i, 0) * 40ms);
}
/* Labels */
.fab-action__label {
font-size: 0.8rem;
font-weight: 500;
color: #f2f6ff;
background: rgba(10, 16, 30, 0.92);
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 0.3rem 0.65rem;
border-radius: 0.4rem;
white-space: nowrap;
pointer-events: none;
letter-spacing: 0.01em;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}
/* Action buttons */
.fab-action__btn {
width: 2.75rem;
height: 2.75rem;
border-radius: 50%;
border: none;
cursor: pointer;
background: #1a2540;
color: #c5d3f0;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.06);
transition: background 0.18s ease, color 0.18s ease, transform 0.18s ease, box-shadow 0.18s ease;
flex-shrink: 0;
}
.fab-action__btn:hover {
background: #243060;
color: #e8eeff;
transform: scale(1.1);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.1);
}
.fab-action__btn:active {
transform: scale(0.96);
}
.fab-action__btn--danger {
background: #2d1520;
color: #f87171;
}
.fab-action__btn--danger:hover {
background: #3d1a25;
color: #fca5a5;
box-shadow: 0 6px 20px rgba(248, 113, 113, 0.25), 0 0 0 1px rgba(248, 113, 113, 0.2);
}
/* ── Main FAB ── */
.fab {
width: 3.5rem;
height: 3.5rem;
border-radius: 50%;
border: none;
cursor: pointer;
background: linear-gradient(135deg, #4f6ef7, #7c3aed);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 6px 24px rgba(79, 110, 247, 0.45), 0 2px 8px rgba(0, 0, 0, 0.4);
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.25s ease;
position: relative;
z-index: 51;
}
.fab:hover {
transform: scale(1.08);
box-shadow: 0 8px 30px rgba(79, 110, 247, 0.6), 0 2px 8px rgba(0, 0, 0, 0.4);
}
.fab:active {
transform: scale(0.94);
}
.fab:focus-visible {
outline: 2px solid #4f6ef7;
outline-offset: 3px;
}
/* Icon rotation on open */
.fab__icon {
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.fab-container.is-open .fab__icon--plus {
transform: rotate(45deg);
}
/* ── Snackbar ── */
.snackbar {
position: fixed;
bottom: 6rem;
left: 50%;
transform: translateX(-50%) translateY(1rem);
background: rgba(20, 30, 50, 0.95);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #c5d3f0;
font-size: 0.85rem;
padding: 0.6rem 1.2rem;
border-radius: 2rem;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease, transform 0.2s ease;
white-space: nowrap;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
z-index: 60;
}
.snackbar.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}(function () {
"use strict";
const container = document.getElementById("fab-container");
const btn = document.getElementById("fab-btn");
const backdrop = document.getElementById("fab-backdrop");
const snackbar = document.getElementById("snackbar");
let snackTimer = null;
/** Toggle the speed dial open / closed */
function toggleDial(open) {
const isOpen = open !== undefined ? open : !container.classList.contains("is-open");
container.classList.toggle("is-open", isOpen);
backdrop.classList.toggle("is-visible", isOpen);
btn.setAttribute("aria-expanded", String(isOpen));
btn.setAttribute("aria-label", isOpen ? "Close actions" : "Open actions");
}
/** Close on backdrop click */
backdrop.addEventListener("click", () => toggleDial(false));
/** Toggle on FAB click */
btn.addEventListener("click", (e) => {
e.stopPropagation();
toggleDial();
});
/** Close on Escape */
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && container.classList.contains("is-open")) {
toggleDial(false);
btn.focus();
}
});
/** Keyboard navigation within the dial */
document.getElementById("fab-actions").addEventListener("keydown", (e) => {
const items = [...document.querySelectorAll(".fab-action__btn")];
const idx = items.indexOf(document.activeElement);
if (e.key === "ArrowUp" && idx > 0) {
e.preventDefault();
items[idx - 1].focus();
}
if (e.key === "ArrowDown" && idx < items.length - 1) {
e.preventDefault();
items[idx + 1].focus();
}
if (e.key === "Tab") {
toggleDial(false);
}
});
/** Show a temporary snackbar message */
function showSnack(msg) {
snackbar.textContent = msg;
snackbar.classList.add("show");
clearTimeout(snackTimer);
snackTimer = setTimeout(() => snackbar.classList.remove("show"), 2200);
}
/** Called by inline onclick handlers */
window.handleAction = function (name) {
toggleDial(false);
btn.focus();
showSnack(`"${name}" selected`);
};
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>FAB Speed Dial</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<!-- Mock page content -->
<div class="demo">
<h2 class="demo-title">FAB Speed Dial</h2>
<p class="demo-sub">Click the button in the bottom-right corner to expand the speed dial.</p>
<div class="mock-content">
<div class="mock-card"></div>
<div class="mock-card"></div>
<div class="mock-card"></div>
</div>
</div>
<!-- Backdrop -->
<div class="fab-backdrop" id="fab-backdrop" aria-hidden="true"></div>
<!-- FAB Speed Dial -->
<div class="fab-container" id="fab-container">
<!-- Sub-actions (rendered first so FAB stays on top) -->
<div class="fab-actions" id="fab-actions" role="menu">
<div class="fab-action" style="--i:3">
<span class="fab-action__label">Share</span>
<button class="fab-action__btn" role="menuitem" aria-label="Share" onclick="handleAction('Share')">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/>
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>
</svg>
</button>
</div>
<div class="fab-action" style="--i:2">
<span class="fab-action__label">Edit</span>
<button class="fab-action__btn" role="menuitem" aria-label="Edit" onclick="handleAction('Edit')">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg>
</button>
</div>
<div class="fab-action" style="--i:1">
<span class="fab-action__label">Bookmark</span>
<button class="fab-action__btn" role="menuitem" aria-label="Bookmark" onclick="handleAction('Bookmark')">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/>
</svg>
</button>
</div>
<div class="fab-action" style="--i:0">
<span class="fab-action__label">Delete</span>
<button class="fab-action__btn fab-action__btn--danger" role="menuitem" aria-label="Delete" onclick="handleAction('Delete')">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/>
<path d="M10 11v6"/><path d="M14 11v6"/>
<path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/>
</svg>
</button>
</div>
</div>
<!-- Main FAB button -->
<button
class="fab"
id="fab-btn"
aria-expanded="false"
aria-controls="fab-actions"
aria-label="Open actions"
>
<svg class="fab__icon fab__icon--plus" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" aria-hidden="true">
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
</svg>
</button>
</div>
<!-- Feedback snackbar -->
<div class="snackbar" id="snackbar" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>FAB Speed Dial
A Material Design-inspired Floating Action Button (FAB) that expands into a speed dial menu on click. Sub-actions fan upward with staggered animation, each with a label tooltip. A semi-transparent backdrop closes the menu when clicked outside.
How it works
- Clicking the FAB toggles an
.is-openclass on the container - Sub-action buttons stagger in using CSS custom property
--ifor delay offsets - The backdrop element captures outside clicks to dismiss the menu
- The FAB icon rotates 45° to signal the open state (× shape)
aria-expandedandaria-labelupdate dynamically for screen readers
Actions
- Share — forward/share icon
- Edit — pencil icon
- Bookmark — bookmark icon
- Delete — trash icon
Keyboard & Accessibility
- FAB is a
<button>witharia-expandedandaria-controls - Each sub-action has a descriptive
aria-label - Focus is trapped within open dial;
Escapecloses it
When to use it
- Mobile-first interfaces needing a compact action hub
- Dashboard pages with contextual quick-actions
- Content management UIs (create, share, organize)