21 / 25
All 25 in 25 CSS Button Groups
- 01CSS Split Button with Anchor-Positioned Popover Menu (Popover API, No JS)
- 02CSS Segmented Control with Motion-Path Comet Thumb (offset-path Arc Travel)
- 03CSS View Transitions Toggle Button Group with Morphing Thumb (Shared-Element FLIP)
- 04CSS Pricing Toggle with Monthly / Yearly Switch (Pure CSS, No JavaScript)
- 05CSS Segmented Control with Sliding Thumb Indicator (iOS-Style, Pure CSS)
- 06CSS Split Button Dropdown with Native <details> (Primary Action + Menu)
- 07CSS Icon Button Group for Toolbar Actions (Accessible Tooltips, Pure CSS)
- 08CSS Pill Radio Button Group with Sliding Indicator (Filter Pills, Pure CSS)
- 09CSS Toggle Button Group with Radio Inputs (Single-Select Alignment Group)
- 10CSS Wallet Connect Button Grid for Web3 Sign-In (MetaMask, WalletConnect, Coinbase)
- 11CSS AI Model Selector Button Group with Provider Badges (LLM Model Picker)
- 12CSS Segmented Control with Liquid Mercury Thumb (Metallic Stretch Animation)
- 13CSS Button Group with Subgrid Underline Beam (Perfectly Aligned Tab Indicator)
- 14CSS Multi-Select Chip Buttons with Acid Glow (Toggleable Tag Filters)
- 15CSS Split Button with Quantum Chevron Reveal (Animated Menu Materialize)
- 16CSS Format Toggle Button Group with Conic Fill (Radial Sweep Select)
- 17CSS Pixel Matrix Button Group with LED Segment Glow (Retro Display Buttons)
- 18CSS Multi-Action Button Group with Gradient Flow Progress (Animated CTA Bar)
- 19CSS Icon Segmented Control with Holographic Sheen (Iridescent View Switcher)
- 20CSS Vertical Button Stack with 3D Hover Depth (Tilting Card Buttons)
- 21CSS Floating Action Button with Radial Sub-Button Menu (Arc Fan-Out, Pure CSS)
- 22CSS Filter Button Group with RGB-Split Glitch Active State (Cyberpunk Toggle)
- 23CSS Pill Button Group with Rotating Conic Plasma Border (Animated Gradient Ring)
- 24CSS Action Button Row with Radar Shockwave Click Feedback (Ripple Pulse)
- 25CSS Date-Range Preset Button Group with Cursor-Following Glow (Spotlight Hover)
CSS Floating Action Button with Radial Sub-Button Menu (Arc Fan-Out, Pure CSS)
A floating action button that fans a ring of sub-actions out along an arc when tapped — each satellite springs to its own orbital position on a staggered delay, and the FAB icon rotates into a close cross. The delightful 'CSS floating action button radial menu' (speed-dial) built with a checkbox toggle, no JavaScript.
Build UI like this from scratch: Flexbox, Grid, animations & responsive CSS
ZTM · The CSS Bootcamp · 30‑day money-back guarantee · Affiliate link
The code
<section class="btn-21" aria-label="Orbital floating action button demo">
<div class="btn-21__fab" role="group" aria-label="Quick create">
<button type="button" class="btn-21__sat" style="--x:0px;--y:-96px" aria-label="New note"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M4 4h12l4 4v12H4zM16 4v4h4"/></svg></button>
<button type="button" class="btn-21__sat" style="--x:-84px;--y:-48px" aria-label="New task"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M4 12l5 5L20 6"/></svg></button>
<button type="button" class="btn-21__sat" style="--x:-96px;--y:44px" aria-label="Upload"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M12 16V5M7 10l5-5 5 5M5 19h14"/></svg></button>
<button type="button" class="btn-21__sat" style="--x:-40px;--y:88px" aria-label="Invite"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2"><circle cx="9" cy="8" r="3.2"/><path d="M3.5 20a5.5 5.5 0 0 1 11 0M17 8v6M14 11h6"/></svg></button>
<input type="checkbox" id="btn-21-t" class="btn-21__in">
<label for="btn-21-t" class="btn-21__main" aria-label="Open quick create menu"><span class="btn-21__plus"></span></label>
</div>
</section><section class="btn-21" aria-label="Orbital floating action button demo">
<div class="btn-21__fab" role="group" aria-label="Quick create">
<button type="button" class="btn-21__sat" style="--x:0px;--y:-96px" aria-label="New note"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M4 4h12l4 4v12H4zM16 4v4h4"/></svg></button>
<button type="button" class="btn-21__sat" style="--x:-84px;--y:-48px" aria-label="New task"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M4 12l5 5L20 6"/></svg></button>
<button type="button" class="btn-21__sat" style="--x:-96px;--y:44px" aria-label="Upload"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M12 16V5M7 10l5-5 5 5M5 19h14"/></svg></button>
<button type="button" class="btn-21__sat" style="--x:-40px;--y:88px" aria-label="Invite"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2"><circle cx="9" cy="8" r="3.2"/><path d="M3.5 20a5.5 5.5 0 0 1 11 0M17 8v6M14 11h6"/></svg></button>
<input type="checkbox" id="btn-21-t" class="btn-21__in">
<label for="btn-21-t" class="btn-21__main" aria-label="Open quick create menu"><span class="btn-21__plus"></span></label>
</div>
</section>.btn-21,
.btn-21 *,
.btn-21 *::before,
.btn-21 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.btn-21 {
--accent: oklch(0.6 0.24 300);
font-family: 'Segoe UI',system-ui,sans-serif;
width: 100%;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: radial-gradient(120% 120% at 70% 70%,#221338,#0c0716);
padding: 44px 20px;
}
.btn-21__fab {
position: relative;
width: 64px;
height: 64px;
}
.btn-21__in {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
}
.btn-21__main {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: var(--accent);
cursor: pointer;
box-shadow: 0 12px 30px -8px var(--accent),0 0 0 0 rgba(255,255,255,.2);
z-index: 3;
transition: transform .3s,box-shadow .3s;
}
.btn-21__main:hover {
transform: scale(1.05);
}
.btn-21__plus {
position: relative;
width: 22px;
height: 22px;
}
.btn-21__plus::before,
.btn-21__plus::after {
content: "";
position: absolute;
left: 50%;
top: 50%;
background: #fff;
border-radius: 2px;
translate: -50% -50%;
transition: transform .35s cubic-bezier(.34,1.4,.4,1);
}
.btn-21__plus::before {
width: 22px;
height: 3px;
}
.btn-21__plus::after {
width: 3px;
height: 22px;
}
.btn-21__fab:has(.btn-21__in:checked) .btn-21__plus::before {
transform: rotate(135deg);
}
.btn-21__fab:has(.btn-21__in:checked) .btn-21__plus::after {
transform: rotate(135deg);
}
.btn-21__sat {
position: absolute;
left: 11px;
top: 11px;
width: 42px;
height: 42px;
display: flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 50%;
background: #2a1f45;
color: #e6d8ff;
cursor: pointer;
z-index: 2;
scale: 0;
translate: 0 0;
box-shadow: 0 8px 20px -8px #000;
transition: translate .4s cubic-bezier(.34,1.5,.5,1),scale .35s cubic-bezier(.34,1.5,.5,1),background .18s;
}
.btn-21__sat:hover {
background: var(--accent);
color: #fff;
}
.btn-21__fab:has(.btn-21__in:checked) .btn-21__sat {
translate: var(--x) var(--y);
scale: 1;
}
.btn-21__fab:has(.btn-21__in:checked) .btn-21__sat:nth-child(1) {
transition-delay: .02s;
}
.btn-21__fab:has(.btn-21__in:checked) .btn-21__sat:nth-child(2) {
transition-delay: .07s;
}
.btn-21__fab:has(.btn-21__in:checked) .btn-21__sat:nth-child(3) {
transition-delay: .12s;
}
.btn-21__fab:has(.btn-21__in:checked) .btn-21__sat:nth-child(4) {
transition-delay: .17s;
}
.btn-21__in:focus-visible + .btn-21__main {
outline: 3px solid #fff;
outline-offset: 3px;
}
.btn-21__sat:focus-visible {
outline: 3px solid #fff;
outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
.btn-21__sat,
.btn-21__plus::before,
.btn-21__plus::after {
transition: scale .2s,translate .2s;
}
.btn-21__fab:has(.btn-21__in:checked) .btn-21__sat {
transition-delay: 0s;
}
}.btn-21,
.btn-21 *,
.btn-21 *::before,
.btn-21 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.btn-21 {
--accent: oklch(0.6 0.24 300);
font-family: 'Segoe UI',system-ui,sans-serif;
width: 100%;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: radial-gradient(120% 120% at 70% 70%,#221338,#0c0716);
padding: 44px 20px;
}
.btn-21__fab {
position: relative;
width: 64px;
height: 64px;
}
.btn-21__in {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
}
.btn-21__main {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: var(--accent);
cursor: pointer;
box-shadow: 0 12px 30px -8px var(--accent),0 0 0 0 rgba(255,255,255,.2);
z-index: 3;
transition: transform .3s,box-shadow .3s;
}
.btn-21__main:hover {
transform: scale(1.05);
}
.btn-21__plus {
position: relative;
width: 22px;
height: 22px;
}
.btn-21__plus::before,
.btn-21__plus::after {
content: "";
position: absolute;
left: 50%;
top: 50%;
background: #fff;
border-radius: 2px;
translate: -50% -50%;
transition: transform .35s cubic-bezier(.34,1.4,.4,1);
}
.btn-21__plus::before {
width: 22px;
height: 3px;
}
.btn-21__plus::after {
width: 3px;
height: 22px;
}
.btn-21__fab:has(.btn-21__in:checked) .btn-21__plus::before {
transform: rotate(135deg);
}
.btn-21__fab:has(.btn-21__in:checked) .btn-21__plus::after {
transform: rotate(135deg);
}
.btn-21__sat {
position: absolute;
left: 11px;
top: 11px;
width: 42px;
height: 42px;
display: flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 50%;
background: #2a1f45;
color: #e6d8ff;
cursor: pointer;
z-index: 2;
scale: 0;
translate: 0 0;
box-shadow: 0 8px 20px -8px #000;
transition: translate .4s cubic-bezier(.34,1.5,.5,1),scale .35s cubic-bezier(.34,1.5,.5,1),background .18s;
}
.btn-21__sat:hover {
background: var(--accent);
color: #fff;
}
.btn-21__fab:has(.btn-21__in:checked) .btn-21__sat {
translate: var(--x) var(--y);
scale: 1;
}
.btn-21__fab:has(.btn-21__in:checked) .btn-21__sat:nth-child(1) {
transition-delay: .02s;
}
.btn-21__fab:has(.btn-21__in:checked) .btn-21__sat:nth-child(2) {
transition-delay: .07s;
}
.btn-21__fab:has(.btn-21__in:checked) .btn-21__sat:nth-child(3) {
transition-delay: .12s;
}
.btn-21__fab:has(.btn-21__in:checked) .btn-21__sat:nth-child(4) {
transition-delay: .17s;
}
.btn-21__in:focus-visible + .btn-21__main {
outline: 3px solid #fff;
outline-offset: 3px;
}
.btn-21__sat:focus-visible {
outline: 3px solid #fff;
outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
.btn-21__sat,
.btn-21__plus::before,
.btn-21__plus::after {
transition: scale .2s,translate .2s;
}
.btn-21__fab:has(.btn-21__in:checked) .btn-21__sat {
transition-delay: 0s;
}
}Here's a working CSS Button Group 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: CSS Floating Action Button with Radial Sub-Button Menu (Arc Fan-Out, Pure CSS)
Source: https://codefronts.com/components/css-button-groups/orbital-fab/
A floating action button that fans a ring of sub-actions out along an arc when tapped — each satellite springs to its own orbital position on a staggered delay, and the FAB icon rotates into a close cross. The delightful 'CSS floating action button radial menu' (speed-dial) built with a checkbox toggle, no JavaScript.
## HTML
```html
<section class="btn-21" aria-label="Orbital floating action button demo">
<div class="btn-21__fab" role="group" aria-label="Quick create">
<button type="button" class="btn-21__sat" style="--x:0px;--y:-96px" aria-label="New note"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M4 4h12l4 4v12H4zM16 4v4h4"/></svg></button>
<button type="button" class="btn-21__sat" style="--x:-84px;--y:-48px" aria-label="New task"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M4 12l5 5L20 6"/></svg></button>
<button type="button" class="btn-21__sat" style="--x:-96px;--y:44px" aria-label="Upload"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M12 16V5M7 10l5-5 5 5M5 19h14"/></svg></button>
<button type="button" class="btn-21__sat" style="--x:-40px;--y:88px" aria-label="Invite"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2"><circle cx="9" cy="8" r="3.2"/><path d="M3.5 20a5.5 5.5 0 0 1 11 0M17 8v6M14 11h6"/></svg></button>
<input type="checkbox" id="btn-21-t" class="btn-21__in">
<label for="btn-21-t" class="btn-21__main" aria-label="Open quick create menu"><span class="btn-21__plus"></span></label>
</div>
</section>
```
## CSS
```css
.btn-21,
.btn-21 *,
.btn-21 *::before,
.btn-21 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.btn-21 {
--accent: oklch(0.6 0.24 300);
font-family: 'Segoe UI',system-ui,sans-serif;
width: 100%;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: radial-gradient(120% 120% at 70% 70%,#221338,#0c0716);
padding: 44px 20px;
}
.btn-21__fab {
position: relative;
width: 64px;
height: 64px;
}
.btn-21__in {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
}
.btn-21__main {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: var(--accent);
cursor: pointer;
box-shadow: 0 12px 30px -8px var(--accent),0 0 0 0 rgba(255,255,255,.2);
z-index: 3;
transition: transform .3s,box-shadow .3s;
}
.btn-21__main:hover {
transform: scale(1.05);
}
.btn-21__plus {
position: relative;
width: 22px;
height: 22px;
}
.btn-21__plus::before,
.btn-21__plus::after {
content: "";
position: absolute;
left: 50%;
top: 50%;
background: #fff;
border-radius: 2px;
translate: -50% -50%;
transition: transform .35s cubic-bezier(.34,1.4,.4,1);
}
.btn-21__plus::before {
width: 22px;
height: 3px;
}
.btn-21__plus::after {
width: 3px;
height: 22px;
}
.btn-21__fab:has(.btn-21__in:checked) .btn-21__plus::before {
transform: rotate(135deg);
}
.btn-21__fab:has(.btn-21__in:checked) .btn-21__plus::after {
transform: rotate(135deg);
}
.btn-21__sat {
position: absolute;
left: 11px;
top: 11px;
width: 42px;
height: 42px;
display: flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 50%;
background: #2a1f45;
color: #e6d8ff;
cursor: pointer;
z-index: 2;
scale: 0;
translate: 0 0;
box-shadow: 0 8px 20px -8px #000;
transition: translate .4s cubic-bezier(.34,1.5,.5,1),scale .35s cubic-bezier(.34,1.5,.5,1),background .18s;
}
.btn-21__sat:hover {
background: var(--accent);
color: #fff;
}
.btn-21__fab:has(.btn-21__in:checked) .btn-21__sat {
translate: var(--x) var(--y);
scale: 1;
}
.btn-21__fab:has(.btn-21__in:checked) .btn-21__sat:nth-child(1) {
transition-delay: .02s;
}
.btn-21__fab:has(.btn-21__in:checked) .btn-21__sat:nth-child(2) {
transition-delay: .07s;
}
.btn-21__fab:has(.btn-21__in:checked) .btn-21__sat:nth-child(3) {
transition-delay: .12s;
}
.btn-21__fab:has(.btn-21__in:checked) .btn-21__sat:nth-child(4) {
transition-delay: .17s;
}
.btn-21__in:focus-visible + .btn-21__main {
outline: 3px solid #fff;
outline-offset: 3px;
}
.btn-21__sat:focus-visible {
outline: 3px solid #fff;
outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
.btn-21__sat,
.btn-21__plus::before,
.btn-21__plus::after {
transition: scale .2s,translate .2s;
}
.btn-21__fab:has(.btn-21__in:checked) .btn-21__sat {
transition-delay: 0s;
}
}
```Here's a working CSS Button Group 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: CSS Floating Action Button with Radial Sub-Button Menu (Arc Fan-Out, Pure CSS)
Source: https://codefronts.com/components/css-button-groups/orbital-fab/
A floating action button that fans a ring of sub-actions out along an arc when tapped — each satellite springs to its own orbital position on a staggered delay, and the FAB icon rotates into a close cross. The delightful 'CSS floating action button radial menu' (speed-dial) built with a checkbox toggle, no JavaScript.
## HTML
```html
<section class="btn-21" aria-label="Orbital floating action button demo">
<div class="btn-21__fab" role="group" aria-label="Quick create">
<button type="button" class="btn-21__sat" style="--x:0px;--y:-96px" aria-label="New note"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M4 4h12l4 4v12H4zM16 4v4h4"/></svg></button>
<button type="button" class="btn-21__sat" style="--x:-84px;--y:-48px" aria-label="New task"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M4 12l5 5L20 6"/></svg></button>
<button type="button" class="btn-21__sat" style="--x:-96px;--y:44px" aria-label="Upload"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M12 16V5M7 10l5-5 5 5M5 19h14"/></svg></button>
<button type="button" class="btn-21__sat" style="--x:-40px;--y:88px" aria-label="Invite"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2"><circle cx="9" cy="8" r="3.2"/><path d="M3.5 20a5.5 5.5 0 0 1 11 0M17 8v6M14 11h6"/></svg></button>
<input type="checkbox" id="btn-21-t" class="btn-21__in">
<label for="btn-21-t" class="btn-21__main" aria-label="Open quick create menu"><span class="btn-21__plus"></span></label>
</div>
</section>
```
## CSS
```css
.btn-21,
.btn-21 *,
.btn-21 *::before,
.btn-21 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.btn-21 {
--accent: oklch(0.6 0.24 300);
font-family: 'Segoe UI',system-ui,sans-serif;
width: 100%;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: radial-gradient(120% 120% at 70% 70%,#221338,#0c0716);
padding: 44px 20px;
}
.btn-21__fab {
position: relative;
width: 64px;
height: 64px;
}
.btn-21__in {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
}
.btn-21__main {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: var(--accent);
cursor: pointer;
box-shadow: 0 12px 30px -8px var(--accent),0 0 0 0 rgba(255,255,255,.2);
z-index: 3;
transition: transform .3s,box-shadow .3s;
}
.btn-21__main:hover {
transform: scale(1.05);
}
.btn-21__plus {
position: relative;
width: 22px;
height: 22px;
}
.btn-21__plus::before,
.btn-21__plus::after {
content: "";
position: absolute;
left: 50%;
top: 50%;
background: #fff;
border-radius: 2px;
translate: -50% -50%;
transition: transform .35s cubic-bezier(.34,1.4,.4,1);
}
.btn-21__plus::before {
width: 22px;
height: 3px;
}
.btn-21__plus::after {
width: 3px;
height: 22px;
}
.btn-21__fab:has(.btn-21__in:checked) .btn-21__plus::before {
transform: rotate(135deg);
}
.btn-21__fab:has(.btn-21__in:checked) .btn-21__plus::after {
transform: rotate(135deg);
}
.btn-21__sat {
position: absolute;
left: 11px;
top: 11px;
width: 42px;
height: 42px;
display: flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 50%;
background: #2a1f45;
color: #e6d8ff;
cursor: pointer;
z-index: 2;
scale: 0;
translate: 0 0;
box-shadow: 0 8px 20px -8px #000;
transition: translate .4s cubic-bezier(.34,1.5,.5,1),scale .35s cubic-bezier(.34,1.5,.5,1),background .18s;
}
.btn-21__sat:hover {
background: var(--accent);
color: #fff;
}
.btn-21__fab:has(.btn-21__in:checked) .btn-21__sat {
translate: var(--x) var(--y);
scale: 1;
}
.btn-21__fab:has(.btn-21__in:checked) .btn-21__sat:nth-child(1) {
transition-delay: .02s;
}
.btn-21__fab:has(.btn-21__in:checked) .btn-21__sat:nth-child(2) {
transition-delay: .07s;
}
.btn-21__fab:has(.btn-21__in:checked) .btn-21__sat:nth-child(3) {
transition-delay: .12s;
}
.btn-21__fab:has(.btn-21__in:checked) .btn-21__sat:nth-child(4) {
transition-delay: .17s;
}
.btn-21__in:focus-visible + .btn-21__main {
outline: 3px solid #fff;
outline-offset: 3px;
}
.btn-21__sat:focus-visible {
outline: 3px solid #fff;
outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
.btn-21__sat,
.btn-21__plus::before,
.btn-21__plus::after {
transition: scale .2s,translate .2s;
}
.btn-21__fab:has(.btn-21__in:checked) .btn-21__sat {
transition-delay: 0s;
}
}
```How this works
A hidden <input type=checkbox> toggles the open state; :has(:checked) on the container animates everything. Each satellite button is absolutely centered on the FAB and, when open, translated to its orbital slot with a per-item --a angle and radius — computed once with calc() using cos/sin via paired X/Y offsets, and released on a transition-delay stagger so they bloom outward in sequence.
Closed, the satellites collapse back under the FAB at scale(0); the main + icon rotates 135° into an ×. A springy cubic-bezier gives each satellite an overshoot as it snaps to orbit — the signature speed-dial feel.
.container:has(:checked) .sat{ translate:var(--x) var(--y); scale:1 }
.sat:nth-child(2){ transition-delay:.04s } /* stagger */Techniques used: checkbox + :has() open state · per-satellite polar offset (--x/--y) placement · staggered transition-delay fan-out · springy overshoot cubic-bezier · icon rotate to close cross · labels on hover
Make it yours
- Orbit radius = the
--x/--yoffset magnitudes per satellite. - Arc spread: change each satellite's target angle/offsets.
- Stagger feel = the per-child
transition-delaystep. --accenttints the FAB and its glow.
Gotchas — read before shipping
- The FAB toggle is a checkbox — for a true menu, add
aria-expanded/rolevia a few lines of JS; for a demo the checkbox semantics are acceptable. - Each satellite needs a real accessible name (aria-label), not just an icon.
- Keep satellites ≥44px and clear of screen edges when placed in a real corner.
- Honor
prefers-reduced-motion: drop the stagger and overshoot, keep a simple fade.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 15.4+ | 121+ | 111+ |
Floor set by :has(), oklch() as this demo is written. The core technique itself goes back further — Chrome 105+.
:has() Baseline 2023. Without it, wire the open class with ~5 lines of JS; the polar layout and stagger are plain transitions.