Open demo

CSS code

HTML
<div class="rad">
  <button type="button" class="rad-fab" aria-expanded="false" aria-controls="rad-actions" aria-label="Quick actions">
    <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>
  </button>
  <nav class="rad-nav" id="rad-actions" aria-label="Quick actions">
    <ul>
      <li><a class="rad-act" href="#" style="--a:180deg;--i:0" aria-label="New document"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 3H7a2 2 0 00-2 2v14a2 2 0 002 2h10a2 2 0 002-2V8z"/><path d="M14 3v5h5"/></svg></a></li>
      <li><a class="rad-act" href="#" style="--a:225deg;--i:1" aria-label="Upload file"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 19V8M7 12l5-5 5 5M5 20h14"/></svg></a></li>
      <li><a class="rad-act" href="#" style="--a:270deg;--i:2" aria-label="Invite people"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="10" cy="8" r="3.4"/><path d="M3 20c0-3.4 3-5.6 7-5.6M18 8v6M21 11h-6"/></svg></a></li>
      <li><a class="rad-act" href="#" style="--a:315deg;--i:3" aria-label="Start a call"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 4h4l2 5-2.5 1.5a12 12 0 005 5L15 13l5 2v4a1 1 0 01-1 1A16 16 0 014 5a1 1 0 011-1z"/></svg></a></li>
      <li><a class="rad-act" href="#" style="--a:0deg;--i:4" aria-label="Schedule an event"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="4" y="5" width="16" height="16" rx="2"/><path d="M8 3v4M16 3v4M4 11h16"/></svg></a></li>
    </ul>
  </nav>
</div>
CSS
/* Radial action menu. The round button (aria-expanded) fans five links out on a half circle: each link is rotated by
   its --a angle, pushed out 104px and rotated back so the icon stays upright; --i staggers them. Closed links are
   visibility:hidden, so they drop out of the tab order. Esc, a click outside or leaving with Tab closes the fan. */
.rad{--rad-accent:#4f46e5;--rad-ink:#101828;--rad-text:#475467;--rad-radius:104px;--rad-bounce:cubic-bezier(.34,1.3,.5,1);
  position:relative;display:grid;place-items:center;width:min(340px,100%);height:300px}
.rad-fab{position:relative;z-index:5;display:grid;place-items:center;width:64px;height:64px;padding:0;border:0;border-radius:50%;
  background:var(--rad-accent);color:#fff;cursor:pointer;box-shadow:0 12px 30px -10px rgba(79,70,229,.85);transition:background-color .18s}
.rad-fab:hover{background:#4338ca}
.rad-fab:focus-visible{outline:2px solid var(--rad-accent);outline-offset:4px}
.rad-fab svg{display:block;width:24px;height:24px;stroke:currentColor;fill:none;stroke-width:2.4;transition:transform .3s}
.rad-fab[aria-expanded="true"] svg{transform:rotate(135deg)}
.rad-nav{position:absolute;inset:0}
.rad-nav ul{margin:0;padding:0;list-style:none}
.rad-act{position:absolute;top:50%;left:50%;display:grid;place-items:center;width:48px;height:48px;margin:-24px 0 0 -24px;border-radius:50%;
  background:#fff;color:var(--rad-text);border:1px solid #e4e7ec;box-shadow:0 8px 22px -10px rgba(16,24,40,.4);
  opacity:0;visibility:hidden;transform:rotate(var(--a)) translate(0) rotate(calc(-1*var(--a))) scale(.4);
  transition:transform .34s var(--rad-bounce) calc(var(--i)*.045s),opacity .22s calc(var(--i)*.045s),visibility 0s .4s,
    background-color .15s,color .15s,border-color .15s}
.rad-fab[aria-expanded="true"]+.rad-nav .rad-act{opacity:1;visibility:visible;transition-delay:calc(var(--i)*.045s),calc(var(--i)*.045s),0s,0s,0s,0s;
  transform:rotate(var(--a)) translate(var(--rad-radius)) rotate(calc(-1*var(--a))) scale(1)}
.rad-act:hover{background:var(--rad-ink);color:#fff;border-color:var(--rad-ink)}
.rad-act:focus-visible{outline:2px solid var(--rad-accent);outline-offset:3px}
.rad-act svg{display:block;width:18px;height:18px;stroke:currentColor;fill:none;stroke-width:2}
@media (prefers-reduced-motion:reduce){.rad-fab,.rad-fab svg,.rad-act,.rad-fab[aria-expanded="true"]+.rad-nav .rad-act{transition:none}}
JavaScript
// Toggle the fan with the button; Esc closes it and returns focus to the button. A click outside, choosing an
// action, or tabbing out of the menu also closes it.
document.querySelectorAll('.rad').forEach(rad => {
  const fab = rad.querySelector('.rad-fab');
  const set = open => fab.setAttribute('aria-expanded', open);
  fab.addEventListener('click', () => set(fab.getAttribute('aria-expanded') !== 'true'));
  rad.addEventListener('keydown', e => {
    if (e.key === 'Escape' && fab.getAttribute('aria-expanded') === 'true') { set(false); fab.focus(); }
  });
  rad.addEventListener('click', e => { if (e.target.closest('.rad-act')) set(false); });
  rad.addEventListener('focusout', e => { if (e.relatedTarget && !rad.contains(e.relatedTarget)) set(false); });
  document.addEventListener('click', e => { if (!rad.contains(e.target)) set(false); });
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS, Bootstrap 5.3.8 JS bundle

HTML
<nav class="dropdown rad" aria-label="Quick actions">
  <button type="button" class="rad-fab" data-bs-toggle="dropdown" data-bs-display="static" data-bs-auto-close="true" aria-expanded="false" aria-label="Quick actions">
    <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>
  </button>
  <ul class="dropdown-menu">
      <li><a class="dropdown-item rad-act" href="#" style="--a:180deg;--i:0" aria-label="New document"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 3H7a2 2 0 00-2 2v14a2 2 0 002 2h10a2 2 0 002-2V8z"/><path d="M14 3v5h5"/></svg></a></li>
      <li><a class="dropdown-item rad-act" href="#" style="--a:225deg;--i:1" aria-label="Upload file"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 19V8M7 12l5-5 5 5M5 20h14"/></svg></a></li>
      <li><a class="dropdown-item rad-act" href="#" style="--a:270deg;--i:2" aria-label="Invite people"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="10" cy="8" r="3.4"/><path d="M3 20c0-3.4 3-5.6 7-5.6M18 8v6M21 11h-6"/></svg></a></li>
      <li><a class="dropdown-item rad-act" href="#" style="--a:315deg;--i:3" aria-label="Start a call"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 4h4l2 5-2.5 1.5a12 12 0 005 5L15 13l5 2v4a1 1 0 01-1 1A16 16 0 014 5a1 1 0 011-1z"/></svg></a></li>
      <li><a class="dropdown-item rad-act" href="#" style="--a:0deg;--i:4" aria-label="Schedule an event"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="4" y="5" width="16" height="16" rx="2"/><path d="M8 3v4M16 3v4M4 11h16"/></svg></a></li>
    </ul>
</nav>
CSS
/* Radial action menu on Bootstrap's Dropdown plugin (data-bs-display="static", so the CSS below places the items).
   The plugin toggles .show and aria-expanded, moves through the actions with the arrow keys, and closes on Esc
   (returning focus to the button), on an outside click or when an action is chosen. Each .dropdown-item is rotated by
   its --a angle, pushed out 104px and rotated back; --i staggers them. */
.rad{position:relative;--rad-accent:#4f46e5;--rad-ink:#101828;--rad-text:#475467;--rad-radius:104px;--rad-bounce:cubic-bezier(.34,1.3,.5,1);
  display:grid;place-items:center;width:min(340px,100%);height:300px}
.rad-fab{position:relative;z-index:5;display:grid;place-items:center;width:64px;height:64px;padding:0;border:0;border-radius:50%;
  background:var(--rad-accent);color:#fff;box-shadow:0 12px 30px -10px rgba(79,70,229,.85);transition:background-color .18s}
.rad-fab:hover{background:#4338ca}
.rad-fab:focus-visible{outline:2px solid var(--rad-accent);outline-offset:4px}
.rad-fab svg{display:block;width:24px;height:24px;stroke:currentColor;fill:none;stroke-width:2.4;transition:transform .3s}
.rad-fab.show svg{transform:rotate(135deg)}
.rad .dropdown-menu,.rad .dropdown-menu[data-bs-popper]{position:absolute;inset:0;display:block;min-width:0;margin:0;padding:0;background:none;border:0;
  box-shadow:none;pointer-events:none}
.rad .rad-act{position:absolute;top:50%;left:50%;display:grid;place-items:center;width:48px;height:48px;margin:-24px 0 0 -24px;padding:0;border-radius:50%;
  background:#fff;color:var(--rad-text);border:1px solid #e4e7ec;box-shadow:0 8px 22px -10px rgba(16,24,40,.4);pointer-events:auto;
  opacity:0;visibility:hidden;transform:rotate(var(--a)) translate(0) rotate(calc(-1*var(--a))) scale(.4);
  transition:transform .34s var(--rad-bounce) calc(var(--i)*.045s),opacity .22s calc(var(--i)*.045s),visibility 0s .4s,
    background-color .15s,color .15s,border-color .15s}
.rad .dropdown-menu.show .rad-act{opacity:1;visibility:visible;transition-delay:calc(var(--i)*.045s),calc(var(--i)*.045s),0s,0s,0s,0s;
  transform:rotate(var(--a)) translate(var(--rad-radius)) rotate(calc(-1*var(--a))) scale(1)}
.rad .rad-act:hover{background:var(--rad-ink);color:#fff;border-color:var(--rad-ink)}
.rad .rad-act:focus-visible{outline:2px solid var(--rad-accent);outline-offset:3px}
.rad-act svg{display:block;width:18px;height:18px;stroke:currentColor;fill:none;stroke-width:2}
@media (prefers-reduced-motion:reduce){.rad-fab,.rad-fab svg,.rad .rad-act,.rad .dropdown-menu.show .rad-act{transition:none}}

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div data-rad class="group relative grid h-[300px] w-[min(340px,100%)] place-items-center">
  <button type="button" class="relative z-5 grid size-16 cursor-pointer place-items-center rounded-full bg-[#4f46e5] text-white shadow-[0_12px_30px_-10px_rgba(79,70,229,.85)] transition-[color,background-color,border-color] duration-[180ms] hover:bg-[#4338ca] focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[#4f46e5] motion-reduce:transition-none" aria-expanded="false" aria-controls="rad-actions" aria-label="Quick actions">
    <svg class="block size-6 fill-none stroke-current stroke-[2.4] transition-transform duration-300 group-data-open:rotate-135 motion-reduce:transition-none" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>
  </button>
  <nav class="absolute inset-0" id="rad-actions" aria-label="Quick actions">
    <ul>
      <li><a class="absolute top-1/2 left-1/2 -mt-6 -ml-6 grid size-12 place-items-center rounded-full border border-[#e4e7ec] bg-white text-[#475467] shadow-[0_8px_22px_-10px_rgba(16,24,40,.4)] invisible opacity-0 [transform:rotate(var(--a))_translate(0)_rotate(calc(-1*var(--a)))_scale(.4)] [transition:transform_.34s_cubic-bezier(.34,1.3,.5,1)_calc(var(--i)*.045s),opacity_.22s_calc(var(--i)*.045s),visibility_0s_.4s,background-color_.15s,color_.15s,border-color_.15s] group-data-open:visible group-data-open:opacity-100 group-data-open:[transform:rotate(var(--a))_translate(104px)_rotate(calc(-1*var(--a)))_scale(1)] group-data-open:[transition-delay:calc(var(--i)*.045s),calc(var(--i)*.045s),0s,0s,0s,0s] hover:border-[#101828] hover:bg-[#101828] hover:text-white focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-[#4f46e5] motion-reduce:transition-none" href="#" style="--a:180deg;--i:0" aria-label="New document"><svg class="block size-[18px] fill-none stroke-current stroke-2" viewBox="0 0 24 24" aria-hidden="true"><path d="M14 3H7a2 2 0 00-2 2v14a2 2 0 002 2h10a2 2 0 002-2V8z"/><path d="M14 3v5h5"/></svg></a></li>
      <li><a class="absolute top-1/2 left-1/2 -mt-6 -ml-6 grid size-12 place-items-center rounded-full border border-[#e4e7ec] bg-white text-[#475467] shadow-[0_8px_22px_-10px_rgba(16,24,40,.4)] invisible opacity-0 [transform:rotate(var(--a))_translate(0)_rotate(calc(-1*var(--a)))_scale(.4)] [transition:transform_.34s_cubic-bezier(.34,1.3,.5,1)_calc(var(--i)*.045s),opacity_.22s_calc(var(--i)*.045s),visibility_0s_.4s,background-color_.15s,color_.15s,border-color_.15s] group-data-open:visible group-data-open:opacity-100 group-data-open:[transform:rotate(var(--a))_translate(104px)_rotate(calc(-1*var(--a)))_scale(1)] group-data-open:[transition-delay:calc(var(--i)*.045s),calc(var(--i)*.045s),0s,0s,0s,0s] hover:border-[#101828] hover:bg-[#101828] hover:text-white focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-[#4f46e5] motion-reduce:transition-none" href="#" style="--a:225deg;--i:1" aria-label="Upload file"><svg class="block size-[18px] fill-none stroke-current stroke-2" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 19V8M7 12l5-5 5 5M5 20h14"/></svg></a></li>
      <li><a class="absolute top-1/2 left-1/2 -mt-6 -ml-6 grid size-12 place-items-center rounded-full border border-[#e4e7ec] bg-white text-[#475467] shadow-[0_8px_22px_-10px_rgba(16,24,40,.4)] invisible opacity-0 [transform:rotate(var(--a))_translate(0)_rotate(calc(-1*var(--a)))_scale(.4)] [transition:transform_.34s_cubic-bezier(.34,1.3,.5,1)_calc(var(--i)*.045s),opacity_.22s_calc(var(--i)*.045s),visibility_0s_.4s,background-color_.15s,color_.15s,border-color_.15s] group-data-open:visible group-data-open:opacity-100 group-data-open:[transform:rotate(var(--a))_translate(104px)_rotate(calc(-1*var(--a)))_scale(1)] group-data-open:[transition-delay:calc(var(--i)*.045s),calc(var(--i)*.045s),0s,0s,0s,0s] hover:border-[#101828] hover:bg-[#101828] hover:text-white focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-[#4f46e5] motion-reduce:transition-none" href="#" style="--a:270deg;--i:2" aria-label="Invite people"><svg class="block size-[18px] fill-none stroke-current stroke-2" viewBox="0 0 24 24" aria-hidden="true"><circle cx="10" cy="8" r="3.4"/><path d="M3 20c0-3.4 3-5.6 7-5.6M18 8v6M21 11h-6"/></svg></a></li>
      <li><a class="absolute top-1/2 left-1/2 -mt-6 -ml-6 grid size-12 place-items-center rounded-full border border-[#e4e7ec] bg-white text-[#475467] shadow-[0_8px_22px_-10px_rgba(16,24,40,.4)] invisible opacity-0 [transform:rotate(var(--a))_translate(0)_rotate(calc(-1*var(--a)))_scale(.4)] [transition:transform_.34s_cubic-bezier(.34,1.3,.5,1)_calc(var(--i)*.045s),opacity_.22s_calc(var(--i)*.045s),visibility_0s_.4s,background-color_.15s,color_.15s,border-color_.15s] group-data-open:visible group-data-open:opacity-100 group-data-open:[transform:rotate(var(--a))_translate(104px)_rotate(calc(-1*var(--a)))_scale(1)] group-data-open:[transition-delay:calc(var(--i)*.045s),calc(var(--i)*.045s),0s,0s,0s,0s] hover:border-[#101828] hover:bg-[#101828] hover:text-white focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-[#4f46e5] motion-reduce:transition-none" href="#" style="--a:315deg;--i:3" aria-label="Start a call"><svg class="block size-[18px] fill-none stroke-current stroke-2" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 4h4l2 5-2.5 1.5a12 12 0 005 5L15 13l5 2v4a1 1 0 01-1 1A16 16 0 014 5a1 1 0 011-1z"/></svg></a></li>
      <li><a class="absolute top-1/2 left-1/2 -mt-6 -ml-6 grid size-12 place-items-center rounded-full border border-[#e4e7ec] bg-white text-[#475467] shadow-[0_8px_22px_-10px_rgba(16,24,40,.4)] invisible opacity-0 [transform:rotate(var(--a))_translate(0)_rotate(calc(-1*var(--a)))_scale(.4)] [transition:transform_.34s_cubic-bezier(.34,1.3,.5,1)_calc(var(--i)*.045s),opacity_.22s_calc(var(--i)*.045s),visibility_0s_.4s,background-color_.15s,color_.15s,border-color_.15s] group-data-open:visible group-data-open:opacity-100 group-data-open:[transform:rotate(var(--a))_translate(104px)_rotate(calc(-1*var(--a)))_scale(1)] group-data-open:[transition-delay:calc(var(--i)*.045s),calc(var(--i)*.045s),0s,0s,0s,0s] hover:border-[#101828] hover:bg-[#101828] hover:text-white focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-[#4f46e5] motion-reduce:transition-none" href="#" style="--a:0deg;--i:4" aria-label="Schedule an event"><svg class="block size-[18px] fill-none stroke-current stroke-2" viewBox="0 0 24 24" aria-hidden="true"><rect x="4" y="5" width="16" height="16" rx="2"/><path d="M8 3v4M16 3v4M4 11h16"/></svg></a></li>
    </ul>
  </nav>
</div>
JavaScript
// Toggle the fan with the button (data-open on the wrapper drives the group-data-open: utilities); Esc closes it and returns focus to the button. A click outside, choosing an
// action, or tabbing out of the menu also closes it.
document.querySelectorAll('[data-rad]').forEach(rad => {
  const fab = rad.querySelector('button[aria-controls]');
  const set = open => { fab.setAttribute('aria-expanded', open); rad.toggleAttribute('data-open', open); };
  fab.addEventListener('click', () => set(fab.getAttribute('aria-expanded') !== 'true'));
  rad.addEventListener('keydown', e => {
    if (e.key === 'Escape' && fab.getAttribute('aria-expanded') === 'true') { set(false); fab.focus(); }
  });
  rad.addEventListener('click', e => { if (e.target.closest('nav a')) set(false); });
  rad.addEventListener('focusout', e => { if (e.relatedTarget && !rad.contains(e.relatedTarget)) set(false); });
  document.addEventListener('click', e => { if (!rad.contains(e.target)) set(false); });
});

About this set

A round indigo action button that fans five circular shortcuts out along a half circle above it: New document, Upload file, Invite people, Start a call and Schedule an event. The plus icon turns into a cross as the actions spring out one after another with a slight overshoot. Use it as a floating quick-create button in a dashboard, a whiteboard or a mobile web app, where a handful of actions should be one tap away without a toolbar. Each action is placed with a single transform: rotate by its –a angle, translate 104px, then rotate back so the icon stays upright, while –i sets its stagger delay. Changing the angles or the radius reshapes the fan. The trigger is a real button that reports aria-expanded, and the actions are named links in a list inside a labelled nav. While the fan is closed the actions are visibility:hidden, so they cannot be reached with Tab. Esc closes the fan and returns focus to the button, and choosing an action, clicking outside or tabbing away also closes it. The Bootstrap version runs the same fan on a Dropdown, which adds arrow-key movement between the actions.

What’s included

  • Five actions fanned on a half circle by one transform each
  • Staggered, springy entrance and a plus that turns into a cross
  • Closed actions removed from the tab order
  • Esc closes the fan and returns focus to the button
  • Bootstrap version uses the Dropdown plugin with arrow-key support

Accessibility

The toggle is a button named Quick actions that reports aria-expanded, and the icon-only actions are links with aria-label names in a list inside a nav labelled Quick actions. Hidden actions are out of the tab order, Esc closes the fan and returns focus to the button, and every control shows an indigo focus outline. Transitions switch off under prefers-reduced-motion.

Customise it

Change --rad-accent, --rad-ink and --rad-radius on .rad, and each action's --a angle to reshape the fan (--i sets the stagger order). In Tailwind the radius is the translate(104px) inside the group-data-open: transform utility.