34 CSS Floating Buttons
07 / 34

All 34 in 34 CSS Floating Buttons

  1. 01Material Design 3 Floating Action Button (FAB)
  2. 02iOS-Style Compose Floating Button
  3. 03WhatsApp Floating Message Button CSS
  4. 04Intercom / Drift Chat Bubble Floating Button
  5. 05Accessible WAI-ARIA Floating Action Button (WCAG 2.2)
  6. 06Extended FAB with Icon and Label (Material Design 3)
  7. 07Speed Dial Radial Menu FAB with Backdrop
  8. 08Pure CSS FAB Speed Dial (No JavaScript)
  9. 09Material Gradient Floating Action Button CSS
  10. 10Vertical and Horizontal Speed Dial FAB Menu CSS
  11. 11Morphing FAB Button That Expands to Pill CSS
  12. 12Scroll-to-Top Floating Button with SVG Progress Ring
  13. 13Floating Chat Widget FAB with Notification Panel
  14. 14Fan and Arc Radial Spread FAB Menu CSS
  15. 15Apple Squircle Superellipse Floating Button CSS
  16. 16Social Share Floating Button Cluster CSS
  17. 17Glassmorphism Floating Action Button with Backdrop Blur
  18. 18Cyberpunk Neon Glow Floating Button CSS
  19. 19Floating Compose Button with Tooltip on Hover CSS
  20. 20Draggable Floating Action Button with Corner Snap CSS
  21. 21Notification Bell Floating Button with Badge Counter
  22. 22Floating Cart Button with Live Badge Counter CSS
  23. 23Magnetic Cursor Follower Floating Button CSS
  24. 24Dark Mode Theme Toggle Floating Button CSS
  25. 25Classic Gradient Circle Plus FAB CSS
  26. 26Pulse Ring Attention-Getter Floating Button CSS
  27. 27Pill-Shaped FAB with Icon and Text Label CSS
  28. 28Neo-Brutalist Hard Shadow Floating Button CSS
  29. 29Floating Button with Loading Spinner State CSS
  30. 30Draggable Floating Button with 6-Dot Grip Handle CSS
  31. 31Voice Recording Microphone Floating Button CSS
  32. 32Aurora Gradient Drift Premium Floating Button CSS
  33. 33Quick Emoji Reply Reaction Floating Button CSS
  34. 34Modern Squircle Rounded Square FAB CSS
Light JSMIT licensed

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.

Live Demo
Try it

Nucleo: 44,000+ SVG icons in five families · All Icons Bundle $149

Nucleo · Affiliate link

Get the Nucleo bundle

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>
.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(); }));
})();
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
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:72deg with 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-end for inset-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 — use translate, 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

ChromeSafariFirefoxEdge
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.

Techniques used in this demo

Search CodeFronts

Loading…