34 CSS Floating Buttons07 / 34
Speed Dial Radial Menu FAB with Backdrop
The speed dial: one FAB, four actions arranged on a quarter-circle, a scrim that dims the page so the choice feels modal, and labels that arrive a beat after their icons. The arrangement is computed in CSS with cos() and sin() from a per-item index, so adding a fifth action is one line of HTML — no re-tuning of translate values.
Published
The code
<section class="fb-07" aria-label="Speed dial radial menu demo">
<div class="fb-07__page">
<p class="fb-07__eyebrow">cos() · sin() · staggered delay</p>
<h2 class="fb-07__title">Four actions on a quarter-circle</h2>
<p class="fb-07__sub">The arc is computed from an index, not hard-coded. Open it and the dial fans out in sequence; close it and the order reverses.</p>
<p class="fb-07__chip">--i · --step:30deg · --r:132px</p>
</div>
<div class="fb-07__scrim" id="fb-07-scrim" hidden></div>
<div class="fb-07__dial" id="fb-07-dial">
<ul class="fb-07__acts" id="fb-07-acts">
<li style="--fb-07-i:0"><button class="fb-07__act" type="button" tabindex="-1"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 7h16v12H4z" fill="none" stroke="currentColor" stroke-width="1.8"/><circle cx="12" cy="13" r="3.2" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M9 7l1.4-2h3.2L15 7" fill="none" stroke="currentColor" stroke-width="1.8"/></svg><span class="fb-07__lab">Photo</span></button></li>
<li style="--fb-07-i:1"><button class="fb-07__act" type="button" tabindex="-1"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6 4h9l4 4v12H6z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M15 4v5h4" fill="none" stroke="currentColor" stroke-width="1.8"/></svg><span class="fb-07__lab">Document</span></button></li>
<li style="--fb-07-i:2"><button class="fb-07__act" type="button" tabindex="-1"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 15a3.2 3.2 0 0 0 3.2-3.2V7a3.2 3.2 0 1 0-6.4 0v4.8A3.2 3.2 0 0 0 12 15z" fill="currentColor"/><path d="M6.5 11.6a5.5 5.5 0 0 0 11 0M12 17.2V21" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg><span class="fb-07__lab">Voice note</span></button></li>
<li style="--fb-07-i:3"><button class="fb-07__act" type="button" tabindex="-1"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="10" r="3" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M12 21s6-5.4 6-10a6 6 0 1 0-12 0c0 4.6 6 10 6 10z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/></svg><span class="fb-07__lab">Location</span></button></li>
</ul>
<button class="fb-07__fab" id="fb-07-fab" type="button" aria-expanded="false" aria-controls="fb-07-acts" aria-label="Open quick actions">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/></svg>
</button>
</div>
</section><section class="fb-07" aria-label="Speed dial radial menu demo">
<div class="fb-07__page">
<p class="fb-07__eyebrow">cos() · sin() · staggered delay</p>
<h2 class="fb-07__title">Four actions on a quarter-circle</h2>
<p class="fb-07__sub">The arc is computed from an index, not hard-coded. Open it and the dial fans out in sequence; close it and the order reverses.</p>
<p class="fb-07__chip">--i · --step:30deg · --r:132px</p>
</div>
<div class="fb-07__scrim" id="fb-07-scrim" hidden></div>
<div class="fb-07__dial" id="fb-07-dial">
<ul class="fb-07__acts" id="fb-07-acts">
<li style="--fb-07-i:0"><button class="fb-07__act" type="button" tabindex="-1"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 7h16v12H4z" fill="none" stroke="currentColor" stroke-width="1.8"/><circle cx="12" cy="13" r="3.2" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M9 7l1.4-2h3.2L15 7" fill="none" stroke="currentColor" stroke-width="1.8"/></svg><span class="fb-07__lab">Photo</span></button></li>
<li style="--fb-07-i:1"><button class="fb-07__act" type="button" tabindex="-1"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6 4h9l4 4v12H6z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M15 4v5h4" fill="none" stroke="currentColor" stroke-width="1.8"/></svg><span class="fb-07__lab">Document</span></button></li>
<li style="--fb-07-i:2"><button class="fb-07__act" type="button" tabindex="-1"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 15a3.2 3.2 0 0 0 3.2-3.2V7a3.2 3.2 0 1 0-6.4 0v4.8A3.2 3.2 0 0 0 12 15z" fill="currentColor"/><path d="M6.5 11.6a5.5 5.5 0 0 0 11 0M12 17.2V21" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg><span class="fb-07__lab">Voice note</span></button></li>
<li style="--fb-07-i:3"><button class="fb-07__act" type="button" tabindex="-1"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="10" r="3" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M12 21s6-5.4 6-10a6 6 0 1 0-12 0c0 4.6 6 10 6 10z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/></svg><span class="fb-07__lab">Location</span></button></li>
</ul>
<button class="fb-07__fab" id="fb-07-fab" type="button" aria-expanded="false" aria-controls="fb-07-acts" aria-label="Open quick actions">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/></svg>
</button>
</div>
</section>.fb-07 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--fb-07-bg);
--fb-07-bg: radial-gradient(110% 80% at 80% 90%,oklch(0.32 0.09 300),oklch(0.17 0.04 285) 70%);
--fb-07-accent: oklch(0.72 0.19 330);
--fb-07-ink: oklch(0.97 0.01 300);
--fb-07-mut: oklch(0.78 0.02 300);
--fb-07-r: clamp(104px,24vw,138px);
font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
color: var(--fb-07-ink);
position: relative;
display: grid;
place-items: center;
padding: clamp(20px,5vw,56px);
overflow: hidden;
}
.fb-07 *,
.fb-07 *::before,
.fb-07 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.fb-07__page {
width: min(100%,54ch);
display: grid;
gap: 12px;
justify-items: center;
text-align: center;
}
.fb-07__eyebrow {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--fb-07-accent);
}
.fb-07__title {
font-size: clamp(26px,4.6vw,44px);
line-height: 1.05;
letter-spacing: -.03em;
font-weight: 700;
text-wrap: balance;
}
.fb-07__sub {
font-size: clamp(13.5px,1.7vw,16.5px);
line-height: 1.62;
color: var(--fb-07-mut);
text-wrap: pretty;
}
.fb-07__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11px;
color: var(--fb-07-mut);
padding: 8px 14px;
border: 1px solid oklch(1 0 0/.18);
border-radius: 99px;
}
.fb-07__scrim {
position: absolute;
inset: 0;
z-index: 4;
background: oklch(0.14 0.03 290/.6);
-webkit-backdrop-filter: blur(3px);
backdrop-filter: blur(3px);
animation: fb-07-fade .24s ease;
}
.fb-07__scrim[hidden] {
display: none;
}
@keyframes fb-07-fade {
from {
opacity: 0;
}
}
.fb-07__dial {
position: absolute;
right: clamp(26px,6vw,64px);
bottom: calc(clamp(26px,6vw,64px) + env(safe-area-inset-bottom));
z-index: 5;
width: 62px;
height: 62px;
}
.fb-07__acts {
list-style: none;
position: absolute;
inset: 0;
}
.fb-07__acts li {
position: absolute;
inset: 0;
--fb-07-a: calc(var(--fb-07-i) * 30deg);
}
.fb-07__act {
position: absolute;
inset: 0;
display: grid;
place-items: center;
width: 52px;
height: 52px;
margin: 5px;
border: 0;
border-radius: 50%;
cursor: pointer;
color: oklch(0.28 0.05 300);
background: oklch(0.98 0.01 300);
box-shadow: 0 8px 22px -8px oklch(0.1 0.03 290/.9);
opacity: 0;
scale: .3;
translate: 0 0;
pointer-events: none;
transition: translate .38s cubic-bezier(.2,1.15,.35,1),scale .34s cubic-bezier(.2,1.3,.35,1),opacity .24s ease;
transition-delay: calc((3 - var(--fb-07-i)) * 45ms);
}
.fb-07__act svg {
width: 23px;
height: 23px;
}
.fb-07__lab {
position: absolute;
right: calc(100% + 12px);
white-space: nowrap;
font-size: 12.5px;
font-weight: 600;
color: var(--fb-07-ink);
background: oklch(0.18 0.03 290/.86);
border: 1px solid oklch(1 0 0/.12);
padding: 5px 11px;
border-radius: 8px;
opacity: 0;
translate: 8px 0;
transition: opacity .2s ease,translate .2s ease;
transition-delay: calc(var(--fb-07-i) * 45ms + 120ms);
}
.fb-07__dial[data-open="true"] .fb-07__act {
opacity: 1;
scale: 1;
pointer-events: auto;
translate: calc(cos(var(--fb-07-a)) * var(--fb-07-r) * -1) calc(sin(var(--fb-07-a)) * var(--fb-07-r) * -1);
transition-delay: calc(var(--fb-07-i) * 55ms);
}
.fb-07__dial[data-open="true"] .fb-07__lab {
opacity: 1;
translate: 0 0;
}
.fb-07__act:hover {
background: var(--fb-07-accent);
color: oklch(0.16 0.04 330);
}
.fb-07__act:focus-visible {
outline: 3px solid var(--fb-07-accent);
outline-offset: 3px;
}
.fb-07__act:active {
scale: .92;
}
.fb-07__fab {
position: absolute;
inset: 0;
display: grid;
place-items: center;
width: 62px;
height: 62px;
border: 0;
border-radius: 50%;
cursor: pointer;
color: oklch(0.15 0.04 330);
background: linear-gradient(150deg,oklch(0.82 0.17 90),var(--fb-07-accent));
box-shadow: 0 14px 34px -10px oklch(0.72 0.19 330/.8),0 2px 6px oklch(0.1 0.03 290/.5);
transition: transform .3s cubic-bezier(.2,1.3,.4,1),box-shadow .3s ease;
}
.fb-07__fab svg {
width: 27px;
height: 27px;
transition: rotate .34s cubic-bezier(.3,1.35,.4,1);
}
.fb-07__fab:hover {
transform: scale(1.05);
}
.fb-07__fab:active {
transform: scale(.93);
}
.fb-07__fab:focus-visible {
outline: 3px solid oklch(0.98 0.01 300);
outline-offset: 4px;
}
.fb-07__fab[aria-expanded="true"] svg {
rotate: 135deg;
}
@media (prefers-reduced-motion: reduce) {
.fb-07 * {
transition-duration: .01ms !important;
animation: none !important;
}
}.fb-07 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--fb-07-bg);
--fb-07-bg: radial-gradient(110% 80% at 80% 90%,oklch(0.32 0.09 300),oklch(0.17 0.04 285) 70%);
--fb-07-accent: oklch(0.72 0.19 330);
--fb-07-ink: oklch(0.97 0.01 300);
--fb-07-mut: oklch(0.78 0.02 300);
--fb-07-r: clamp(104px,24vw,138px);
font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
color: var(--fb-07-ink);
position: relative;
display: grid;
place-items: center;
padding: clamp(20px,5vw,56px);
overflow: hidden;
}
.fb-07 *,
.fb-07 *::before,
.fb-07 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.fb-07__page {
width: min(100%,54ch);
display: grid;
gap: 12px;
justify-items: center;
text-align: center;
}
.fb-07__eyebrow {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--fb-07-accent);
}
.fb-07__title {
font-size: clamp(26px,4.6vw,44px);
line-height: 1.05;
letter-spacing: -.03em;
font-weight: 700;
text-wrap: balance;
}
.fb-07__sub {
font-size: clamp(13.5px,1.7vw,16.5px);
line-height: 1.62;
color: var(--fb-07-mut);
text-wrap: pretty;
}
.fb-07__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11px;
color: var(--fb-07-mut);
padding: 8px 14px;
border: 1px solid oklch(1 0 0/.18);
border-radius: 99px;
}
.fb-07__scrim {
position: absolute;
inset: 0;
z-index: 4;
background: oklch(0.14 0.03 290/.6);
-webkit-backdrop-filter: blur(3px);
backdrop-filter: blur(3px);
animation: fb-07-fade .24s ease;
}
.fb-07__scrim[hidden] {
display: none;
}
@keyframes fb-07-fade {
from {
opacity: 0;
}
}
.fb-07__dial {
position: absolute;
right: clamp(26px,6vw,64px);
bottom: calc(clamp(26px,6vw,64px) + env(safe-area-inset-bottom));
z-index: 5;
width: 62px;
height: 62px;
}
.fb-07__acts {
list-style: none;
position: absolute;
inset: 0;
}
.fb-07__acts li {
position: absolute;
inset: 0;
--fb-07-a: calc(var(--fb-07-i) * 30deg);
}
.fb-07__act {
position: absolute;
inset: 0;
display: grid;
place-items: center;
width: 52px;
height: 52px;
margin: 5px;
border: 0;
border-radius: 50%;
cursor: pointer;
color: oklch(0.28 0.05 300);
background: oklch(0.98 0.01 300);
box-shadow: 0 8px 22px -8px oklch(0.1 0.03 290/.9);
opacity: 0;
scale: .3;
translate: 0 0;
pointer-events: none;
transition: translate .38s cubic-bezier(.2,1.15,.35,1),scale .34s cubic-bezier(.2,1.3,.35,1),opacity .24s ease;
transition-delay: calc((3 - var(--fb-07-i)) * 45ms);
}
.fb-07__act svg {
width: 23px;
height: 23px;
}
.fb-07__lab {
position: absolute;
right: calc(100% + 12px);
white-space: nowrap;
font-size: 12.5px;
font-weight: 600;
color: var(--fb-07-ink);
background: oklch(0.18 0.03 290/.86);
border: 1px solid oklch(1 0 0/.12);
padding: 5px 11px;
border-radius: 8px;
opacity: 0;
translate: 8px 0;
transition: opacity .2s ease,translate .2s ease;
transition-delay: calc(var(--fb-07-i) * 45ms + 120ms);
}
.fb-07__dial[data-open="true"] .fb-07__act {
opacity: 1;
scale: 1;
pointer-events: auto;
translate: calc(cos(var(--fb-07-a)) * var(--fb-07-r) * -1) calc(sin(var(--fb-07-a)) * var(--fb-07-r) * -1);
transition-delay: calc(var(--fb-07-i) * 55ms);
}
.fb-07__dial[data-open="true"] .fb-07__lab {
opacity: 1;
translate: 0 0;
}
.fb-07__act:hover {
background: var(--fb-07-accent);
color: oklch(0.16 0.04 330);
}
.fb-07__act:focus-visible {
outline: 3px solid var(--fb-07-accent);
outline-offset: 3px;
}
.fb-07__act:active {
scale: .92;
}
.fb-07__fab {
position: absolute;
inset: 0;
display: grid;
place-items: center;
width: 62px;
height: 62px;
border: 0;
border-radius: 50%;
cursor: pointer;
color: oklch(0.15 0.04 330);
background: linear-gradient(150deg,oklch(0.82 0.17 90),var(--fb-07-accent));
box-shadow: 0 14px 34px -10px oklch(0.72 0.19 330/.8),0 2px 6px oklch(0.1 0.03 290/.5);
transition: transform .3s cubic-bezier(.2,1.3,.4,1),box-shadow .3s ease;
}
.fb-07__fab svg {
width: 27px;
height: 27px;
transition: rotate .34s cubic-bezier(.3,1.35,.4,1);
}
.fb-07__fab:hover {
transform: scale(1.05);
}
.fb-07__fab:active {
transform: scale(.93);
}
.fb-07__fab:focus-visible {
outline: 3px solid oklch(0.98 0.01 300);
outline-offset: 4px;
}
.fb-07__fab[aria-expanded="true"] svg {
rotate: 135deg;
}
@media (prefers-reduced-motion: reduce) {
.fb-07 * {
transition-duration: .01ms !important;
animation: none !important;
}
}(() => {
const dial = document.getElementById('fb-07-dial');
const fab = document.getElementById('fb-07-fab');
const scrim = document.getElementById('fb-07-scrim');
if (!dial || !fab || fab.dataset.fbWired) return;
fab.dataset.fbWired = '1';
const acts = [...dial.querySelectorAll('.fb-07__act')];
const set = (open) => {
dial.dataset.open = String(open);
fab.setAttribute('aria-expanded', String(open));
fab.setAttribute('aria-label', open ? 'Close quick actions' : 'Open quick actions');
scrim.hidden = !open;
acts.forEach((a) => { a.tabIndex = open ? 0 : -1; });
if (open) setTimeout(() => acts[0].focus(), 120);
};
fab.addEventListener('click', () => set(dial.dataset.open !== 'true'));
scrim.addEventListener('click', () => { set(false); fab.focus(); });
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && dial.dataset.open === 'true') { set(false); fab.focus(); }
});
acts.forEach((a) => a.addEventListener('click', () => { set(false); fab.focus(); }));
})();(() => {
const dial = document.getElementById('fb-07-dial');
const fab = document.getElementById('fb-07-fab');
const scrim = document.getElementById('fb-07-scrim');
if (!dial || !fab || fab.dataset.fbWired) return;
fab.dataset.fbWired = '1';
const acts = [...dial.querySelectorAll('.fb-07__act')];
const set = (open) => {
dial.dataset.open = String(open);
fab.setAttribute('aria-expanded', String(open));
fab.setAttribute('aria-label', open ? 'Close quick actions' : 'Open quick actions');
scrim.hidden = !open;
acts.forEach((a) => { a.tabIndex = open ? 0 : -1; });
if (open) setTimeout(() => acts[0].focus(), 120);
};
fab.addEventListener('click', () => set(dial.dataset.open !== 'true'));
scrim.addEventListener('click', () => { set(false); fab.focus(); });
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && dial.dataset.open === 'true') { set(false); fab.focus(); }
});
acts.forEach((a) => a.addEventListener('click', () => { set(false); fab.focus(); }));
})();Here's a working CSS Floating Button from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Speed Dial Radial Menu FAB with Backdrop
Source: https://codefronts.com/components/css-floating-buttons/speed-dial-radial-menu-fab-with-backdrop/
The speed dial: one FAB, four actions arranged on a quarter-circle, a scrim that dims the page so the choice feels modal, and labels that arrive a beat after their icons. The arrangement is computed in CSS with cos() and sin() from a per-item index, so adding a fifth action is one line of HTML — no re-tuning of translate values.
## HTML
```html
<section class="fb-07" aria-label="Speed dial radial menu demo">
<div class="fb-07__page">
<p class="fb-07__eyebrow">cos() · sin() · staggered delay</p>
<h2 class="fb-07__title">Four actions on a quarter-circle</h2>
<p class="fb-07__sub">The arc is computed from an index, not hard-coded. Open it and the dial fans out in sequence; close it and the order reverses.</p>
<p class="fb-07__chip">--i · --step:30deg · --r:132px</p>
</div>
<div class="fb-07__scrim" id="fb-07-scrim" hidden></div>
<div class="fb-07__dial" id="fb-07-dial">
<ul class="fb-07__acts" id="fb-07-acts">
<li style="--fb-07-i:0"><button class="fb-07__act" type="button" tabindex="-1"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 7h16v12H4z" fill="none" stroke="currentColor" stroke-width="1.8"/><circle cx="12" cy="13" r="3.2" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M9 7l1.4-2h3.2L15 7" fill="none" stroke="currentColor" stroke-width="1.8"/></svg><span class="fb-07__lab">Photo</span></button></li>
<li style="--fb-07-i:1"><button class="fb-07__act" type="button" tabindex="-1"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6 4h9l4 4v12H6z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M15 4v5h4" fill="none" stroke="currentColor" stroke-width="1.8"/></svg><span class="fb-07__lab">Document</span></button></li>
<li style="--fb-07-i:2"><button class="fb-07__act" type="button" tabindex="-1"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 15a3.2 3.2 0 0 0 3.2-3.2V7a3.2 3.2 0 1 0-6.4 0v4.8A3.2 3.2 0 0 0 12 15z" fill="currentColor"/><path d="M6.5 11.6a5.5 5.5 0 0 0 11 0M12 17.2V21" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg><span class="fb-07__lab">Voice note</span></button></li>
<li style="--fb-07-i:3"><button class="fb-07__act" type="button" tabindex="-1"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="10" r="3" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M12 21s6-5.4 6-10a6 6 0 1 0-12 0c0 4.6 6 10 6 10z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/></svg><span class="fb-07__lab">Location</span></button></li>
</ul>
<button class="fb-07__fab" id="fb-07-fab" type="button" aria-expanded="false" aria-controls="fb-07-acts" aria-label="Open quick actions">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/></svg>
</button>
</div>
</section>
```
## CSS
```css
.fb-07 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--fb-07-bg);
--fb-07-bg: radial-gradient(110% 80% at 80% 90%,oklch(0.32 0.09 300),oklch(0.17 0.04 285) 70%);
--fb-07-accent: oklch(0.72 0.19 330);
--fb-07-ink: oklch(0.97 0.01 300);
--fb-07-mut: oklch(0.78 0.02 300);
--fb-07-r: clamp(104px,24vw,138px);
font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
color: var(--fb-07-ink);
position: relative;
display: grid;
place-items: center;
padding: clamp(20px,5vw,56px);
overflow: hidden;
}
.fb-07 *,
.fb-07 *::before,
.fb-07 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.fb-07__page {
width: min(100%,54ch);
display: grid;
gap: 12px;
justify-items: center;
text-align: center;
}
.fb-07__eyebrow {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--fb-07-accent);
}
.fb-07__title {
font-size: clamp(26px,4.6vw,44px);
line-height: 1.05;
letter-spacing: -.03em;
font-weight: 700;
text-wrap: balance;
}
.fb-07__sub {
font-size: clamp(13.5px,1.7vw,16.5px);
line-height: 1.62;
color: var(--fb-07-mut);
text-wrap: pretty;
}
.fb-07__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11px;
color: var(--fb-07-mut);
padding: 8px 14px;
border: 1px solid oklch(1 0 0/.18);
border-radius: 99px;
}
.fb-07__scrim {
position: absolute;
inset: 0;
z-index: 4;
background: oklch(0.14 0.03 290/.6);
-webkit-backdrop-filter: blur(3px);
backdrop-filter: blur(3px);
animation: fb-07-fade .24s ease;
}
.fb-07__scrim[hidden] {
display: none;
}
@keyframes fb-07-fade {
from {
opacity: 0;
}
}
.fb-07__dial {
position: absolute;
right: clamp(26px,6vw,64px);
bottom: calc(clamp(26px,6vw,64px) + env(safe-area-inset-bottom));
z-index: 5;
width: 62px;
height: 62px;
}
.fb-07__acts {
list-style: none;
position: absolute;
inset: 0;
}
.fb-07__acts li {
position: absolute;
inset: 0;
--fb-07-a: calc(var(--fb-07-i) * 30deg);
}
.fb-07__act {
position: absolute;
inset: 0;
display: grid;
place-items: center;
width: 52px;
height: 52px;
margin: 5px;
border: 0;
border-radius: 50%;
cursor: pointer;
color: oklch(0.28 0.05 300);
background: oklch(0.98 0.01 300);
box-shadow: 0 8px 22px -8px oklch(0.1 0.03 290/.9);
opacity: 0;
scale: .3;
translate: 0 0;
pointer-events: none;
transition: translate .38s cubic-bezier(.2,1.15,.35,1),scale .34s cubic-bezier(.2,1.3,.35,1),opacity .24s ease;
transition-delay: calc((3 - var(--fb-07-i)) * 45ms);
}
.fb-07__act svg {
width: 23px;
height: 23px;
}
.fb-07__lab {
position: absolute;
right: calc(100% + 12px);
white-space: nowrap;
font-size: 12.5px;
font-weight: 600;
color: var(--fb-07-ink);
background: oklch(0.18 0.03 290/.86);
border: 1px solid oklch(1 0 0/.12);
padding: 5px 11px;
border-radius: 8px;
opacity: 0;
translate: 8px 0;
transition: opacity .2s ease,translate .2s ease;
transition-delay: calc(var(--fb-07-i) * 45ms + 120ms);
}
.fb-07__dial[data-open="true"] .fb-07__act {
opacity: 1;
scale: 1;
pointer-events: auto;
translate: calc(cos(var(--fb-07-a)) * var(--fb-07-r) * -1) calc(sin(var(--fb-07-a)) * var(--fb-07-r) * -1);
transition-delay: calc(var(--fb-07-i) * 55ms);
}
.fb-07__dial[data-open="true"] .fb-07__lab {
opacity: 1;
translate: 0 0;
}
.fb-07__act:hover {
background: var(--fb-07-accent);
color: oklch(0.16 0.04 330);
}
.fb-07__act:focus-visible {
outline: 3px solid var(--fb-07-accent);
outline-offset: 3px;
}
.fb-07__act:active {
scale: .92;
}
.fb-07__fab {
position: absolute;
inset: 0;
display: grid;
place-items: center;
width: 62px;
height: 62px;
border: 0;
border-radius: 50%;
cursor: pointer;
color: oklch(0.15 0.04 330);
background: linear-gradient(150deg,oklch(0.82 0.17 90),var(--fb-07-accent));
box-shadow: 0 14px 34px -10px oklch(0.72 0.19 330/.8),0 2px 6px oklch(0.1 0.03 290/.5);
transition: transform .3s cubic-bezier(.2,1.3,.4,1),box-shadow .3s ease;
}
.fb-07__fab svg {
width: 27px;
height: 27px;
transition: rotate .34s cubic-bezier(.3,1.35,.4,1);
}
.fb-07__fab:hover {
transform: scale(1.05);
}
.fb-07__fab:active {
transform: scale(.93);
}
.fb-07__fab:focus-visible {
outline: 3px solid oklch(0.98 0.01 300);
outline-offset: 4px;
}
.fb-07__fab[aria-expanded="true"] svg {
rotate: 135deg;
}
@media (prefers-reduced-motion: reduce) {
.fb-07 * {
transition-duration: .01ms !important;
animation: none !important;
}
}
```
## JavaScript
```js
(() => {
const dial = document.getElementById('fb-07-dial');
const fab = document.getElementById('fb-07-fab');
const scrim = document.getElementById('fb-07-scrim');
if (!dial || !fab || fab.dataset.fbWired) return;
fab.dataset.fbWired = '1';
const acts = [...dial.querySelectorAll('.fb-07__act')];
const set = (open) => {
dial.dataset.open = String(open);
fab.setAttribute('aria-expanded', String(open));
fab.setAttribute('aria-label', open ? 'Close quick actions' : 'Open quick actions');
scrim.hidden = !open;
acts.forEach((a) => { a.tabIndex = open ? 0 : -1; });
if (open) setTimeout(() => acts[0].focus(), 120);
};
fab.addEventListener('click', () => set(dial.dataset.open !== 'true'));
scrim.addEventListener('click', () => { set(false); fab.focus(); });
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && dial.dataset.open === 'true') { set(false); fab.focus(); }
});
acts.forEach((a) => a.addEventListener('click', () => { set(false); fab.focus(); }));
})();
```Here's a working CSS Floating Button from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Speed Dial Radial Menu FAB with Backdrop
Source: https://codefronts.com/components/css-floating-buttons/speed-dial-radial-menu-fab-with-backdrop/
The speed dial: one FAB, four actions arranged on a quarter-circle, a scrim that dims the page so the choice feels modal, and labels that arrive a beat after their icons. The arrangement is computed in CSS with cos() and sin() from a per-item index, so adding a fifth action is one line of HTML — no re-tuning of translate values.
## HTML
```html
<section class="fb-07" aria-label="Speed dial radial menu demo">
<div class="fb-07__page">
<p class="fb-07__eyebrow">cos() · sin() · staggered delay</p>
<h2 class="fb-07__title">Four actions on a quarter-circle</h2>
<p class="fb-07__sub">The arc is computed from an index, not hard-coded. Open it and the dial fans out in sequence; close it and the order reverses.</p>
<p class="fb-07__chip">--i · --step:30deg · --r:132px</p>
</div>
<div class="fb-07__scrim" id="fb-07-scrim" hidden></div>
<div class="fb-07__dial" id="fb-07-dial">
<ul class="fb-07__acts" id="fb-07-acts">
<li style="--fb-07-i:0"><button class="fb-07__act" type="button" tabindex="-1"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 7h16v12H4z" fill="none" stroke="currentColor" stroke-width="1.8"/><circle cx="12" cy="13" r="3.2" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M9 7l1.4-2h3.2L15 7" fill="none" stroke="currentColor" stroke-width="1.8"/></svg><span class="fb-07__lab">Photo</span></button></li>
<li style="--fb-07-i:1"><button class="fb-07__act" type="button" tabindex="-1"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6 4h9l4 4v12H6z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M15 4v5h4" fill="none" stroke="currentColor" stroke-width="1.8"/></svg><span class="fb-07__lab">Document</span></button></li>
<li style="--fb-07-i:2"><button class="fb-07__act" type="button" tabindex="-1"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 15a3.2 3.2 0 0 0 3.2-3.2V7a3.2 3.2 0 1 0-6.4 0v4.8A3.2 3.2 0 0 0 12 15z" fill="currentColor"/><path d="M6.5 11.6a5.5 5.5 0 0 0 11 0M12 17.2V21" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg><span class="fb-07__lab">Voice note</span></button></li>
<li style="--fb-07-i:3"><button class="fb-07__act" type="button" tabindex="-1"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="10" r="3" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M12 21s6-5.4 6-10a6 6 0 1 0-12 0c0 4.6 6 10 6 10z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/></svg><span class="fb-07__lab">Location</span></button></li>
</ul>
<button class="fb-07__fab" id="fb-07-fab" type="button" aria-expanded="false" aria-controls="fb-07-acts" aria-label="Open quick actions">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/></svg>
</button>
</div>
</section>
```
## CSS
```css
.fb-07 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--fb-07-bg);
--fb-07-bg: radial-gradient(110% 80% at 80% 90%,oklch(0.32 0.09 300),oklch(0.17 0.04 285) 70%);
--fb-07-accent: oklch(0.72 0.19 330);
--fb-07-ink: oklch(0.97 0.01 300);
--fb-07-mut: oklch(0.78 0.02 300);
--fb-07-r: clamp(104px,24vw,138px);
font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
color: var(--fb-07-ink);
position: relative;
display: grid;
place-items: center;
padding: clamp(20px,5vw,56px);
overflow: hidden;
}
.fb-07 *,
.fb-07 *::before,
.fb-07 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.fb-07__page {
width: min(100%,54ch);
display: grid;
gap: 12px;
justify-items: center;
text-align: center;
}
.fb-07__eyebrow {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--fb-07-accent);
}
.fb-07__title {
font-size: clamp(26px,4.6vw,44px);
line-height: 1.05;
letter-spacing: -.03em;
font-weight: 700;
text-wrap: balance;
}
.fb-07__sub {
font-size: clamp(13.5px,1.7vw,16.5px);
line-height: 1.62;
color: var(--fb-07-mut);
text-wrap: pretty;
}
.fb-07__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11px;
color: var(--fb-07-mut);
padding: 8px 14px;
border: 1px solid oklch(1 0 0/.18);
border-radius: 99px;
}
.fb-07__scrim {
position: absolute;
inset: 0;
z-index: 4;
background: oklch(0.14 0.03 290/.6);
-webkit-backdrop-filter: blur(3px);
backdrop-filter: blur(3px);
animation: fb-07-fade .24s ease;
}
.fb-07__scrim[hidden] {
display: none;
}
@keyframes fb-07-fade {
from {
opacity: 0;
}
}
.fb-07__dial {
position: absolute;
right: clamp(26px,6vw,64px);
bottom: calc(clamp(26px,6vw,64px) + env(safe-area-inset-bottom));
z-index: 5;
width: 62px;
height: 62px;
}
.fb-07__acts {
list-style: none;
position: absolute;
inset: 0;
}
.fb-07__acts li {
position: absolute;
inset: 0;
--fb-07-a: calc(var(--fb-07-i) * 30deg);
}
.fb-07__act {
position: absolute;
inset: 0;
display: grid;
place-items: center;
width: 52px;
height: 52px;
margin: 5px;
border: 0;
border-radius: 50%;
cursor: pointer;
color: oklch(0.28 0.05 300);
background: oklch(0.98 0.01 300);
box-shadow: 0 8px 22px -8px oklch(0.1 0.03 290/.9);
opacity: 0;
scale: .3;
translate: 0 0;
pointer-events: none;
transition: translate .38s cubic-bezier(.2,1.15,.35,1),scale .34s cubic-bezier(.2,1.3,.35,1),opacity .24s ease;
transition-delay: calc((3 - var(--fb-07-i)) * 45ms);
}
.fb-07__act svg {
width: 23px;
height: 23px;
}
.fb-07__lab {
position: absolute;
right: calc(100% + 12px);
white-space: nowrap;
font-size: 12.5px;
font-weight: 600;
color: var(--fb-07-ink);
background: oklch(0.18 0.03 290/.86);
border: 1px solid oklch(1 0 0/.12);
padding: 5px 11px;
border-radius: 8px;
opacity: 0;
translate: 8px 0;
transition: opacity .2s ease,translate .2s ease;
transition-delay: calc(var(--fb-07-i) * 45ms + 120ms);
}
.fb-07__dial[data-open="true"] .fb-07__act {
opacity: 1;
scale: 1;
pointer-events: auto;
translate: calc(cos(var(--fb-07-a)) * var(--fb-07-r) * -1) calc(sin(var(--fb-07-a)) * var(--fb-07-r) * -1);
transition-delay: calc(var(--fb-07-i) * 55ms);
}
.fb-07__dial[data-open="true"] .fb-07__lab {
opacity: 1;
translate: 0 0;
}
.fb-07__act:hover {
background: var(--fb-07-accent);
color: oklch(0.16 0.04 330);
}
.fb-07__act:focus-visible {
outline: 3px solid var(--fb-07-accent);
outline-offset: 3px;
}
.fb-07__act:active {
scale: .92;
}
.fb-07__fab {
position: absolute;
inset: 0;
display: grid;
place-items: center;
width: 62px;
height: 62px;
border: 0;
border-radius: 50%;
cursor: pointer;
color: oklch(0.15 0.04 330);
background: linear-gradient(150deg,oklch(0.82 0.17 90),var(--fb-07-accent));
box-shadow: 0 14px 34px -10px oklch(0.72 0.19 330/.8),0 2px 6px oklch(0.1 0.03 290/.5);
transition: transform .3s cubic-bezier(.2,1.3,.4,1),box-shadow .3s ease;
}
.fb-07__fab svg {
width: 27px;
height: 27px;
transition: rotate .34s cubic-bezier(.3,1.35,.4,1);
}
.fb-07__fab:hover {
transform: scale(1.05);
}
.fb-07__fab:active {
transform: scale(.93);
}
.fb-07__fab:focus-visible {
outline: 3px solid oklch(0.98 0.01 300);
outline-offset: 4px;
}
.fb-07__fab[aria-expanded="true"] svg {
rotate: 135deg;
}
@media (prefers-reduced-motion: reduce) {
.fb-07 * {
transition-duration: .01ms !important;
animation: none !important;
}
}
```
## JavaScript
```js
(() => {
const dial = document.getElementById('fb-07-dial');
const fab = document.getElementById('fb-07-fab');
const scrim = document.getElementById('fb-07-scrim');
if (!dial || !fab || fab.dataset.fbWired) return;
fab.dataset.fbWired = '1';
const acts = [...dial.querySelectorAll('.fb-07__act')];
const set = (open) => {
dial.dataset.open = String(open);
fab.setAttribute('aria-expanded', String(open));
fab.setAttribute('aria-label', open ? 'Close quick actions' : 'Open quick actions');
scrim.hidden = !open;
acts.forEach((a) => { a.tabIndex = open ? 0 : -1; });
if (open) setTimeout(() => acts[0].focus(), 120);
};
fab.addEventListener('click', () => set(dial.dataset.open !== 'true'));
scrim.addEventListener('click', () => { set(false); fab.focus(); });
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && dial.dataset.open === 'true') { set(false); fab.focus(); }
});
acts.forEach((a) => a.addEventListener('click', () => { set(false); fab.focus(); }));
})();
```How this works
Each action carries an index; CSS does the trigonometry:
.action{ --a:calc(var(--i) * var(--step) + var(--start));
translate:calc(cos(var(--a)) * var(--r) * -1) calc(sin(var(--a)) * var(--r) * -1);
transition-delay:calc(var(--i) * 55ms); }With --start:0deg, --step:30deg and --r:132px, four items fan from due-left to due-up around the FAB. Change --step to spread them wider, change --r to push them further out, and the layout re-solves itself. Before CSS trig functions this needed either a JS loop or four hand-computed translate pairs per breakpoint.
The stagger is transition-delay multiplied by the index on open, and reversed on close ((count - 1 - i)) so the dial closes from the outside in. Choreography that runs the same direction both ways feels mechanical; reversing it costs one calc.
The scrim is a sibling with backdrop-filter:blur(3px) that fades in under the dial and above the page. It closes on click, Escape closes from anywhere, and while it is open the FAB's plus icon rotates 45° into a close glyph.
Make it yours
- Full circle:
--start:-30deg; --step:72degwith five items gives a complete radial burst around a centred FAB. - Radius per breakpoint:
--r:clamp(96px,22vw,150px)keeps the dial inside small viewports without media queries. - Labels: they are positioned absolutely to the left of each action; flip to the right for a left-anchored FAB by swapping
inset-inline-endforinset-inline-start. - Colour per action: each item accepts its own
--tint, useful when the actions map to categories users already recognise by colour.
Gotchas — read before shipping
- A radial dial anchored bottom-right can push items off-screen on narrow phones. Clamp the radius, or fall back to a vertical stack under 380px.
- Do not animate
left/top— usetranslate, which stays on the compositor. Radial menus animate several elements at once and layout thrash shows immediately. - The scrim must not be tabbable. Give it no tabindex and make sure the actions come before it in DOM order so focus flows naturally.
- CSS trig functions round to device pixels; at very small radii items can appear to overlap. Below ~80px radius, hand-place them.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 114+ | 17.5+ | 121+ | 114+ |
Floor set by oklch(), backdrop-filter, text-wrap as this demo is written. The core technique itself goes back further — Chrome 111+.
cos() and sin() in calc() need Chrome 111, Safari 15.4, Firefox 108. For older engines, set an explicit --x/--y pair per item as a fallback — the rest of the pattern is baseline CSS.