34 CSS Floating Buttons08 / 34
Pure CSS FAB Speed Dial (No JavaScript)
A speed dial with a real open/close state, light dismiss, Escape handling and top-layer stacking — and not a line of script. The popover attribute gives you the behaviour for free, CSS anchor positioning places the menu against the button, and @starting-style animates it in from display:none. This is the pattern that finally makes the old checkbox hack obsolete.
Published
The code
<section class="fb-08" aria-label="Pure CSS speed dial demo">
<div class="fb-08__page">
<p class="fb-08__eyebrow">popover · anchor() · @starting-style</p>
<h2 class="fb-08__title">Top layer, light dismiss, zero script</h2>
<p class="fb-08__sub">Click the button, then click anywhere outside — the browser closes it, restores focus and keeps <code>aria-expanded</code> in sync on its own.</p>
<p class="fb-08__chip">popovertarget · :popover-open · allow-discrete</p>
</div>
<div class="fb-08__dock">
<ul class="fb-08__dial" id="fb-08-dial" popover>
<li><button class="fb-08__act" type="button"><span class="fb-08__ico" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></span>New task</button></li>
<li><button class="fb-08__act" type="button"><span class="fb-08__ico" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><rect x="4" y="5" width="16" height="15" rx="2.5" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M8 3v4M16 3v4M4 10h16" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg></span>Schedule</button></li>
<li><button class="fb-08__act" type="button"><span class="fb-08__ico" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path d="M4 6h16M4 12h16M4 18h10" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg></span>New list</button></li>
<li><button class="fb-08__act" type="button"><span class="fb-08__ico" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path d="M12 3l2.4 6.2H21l-5.3 3.9 2 6.4-5.7-4-5.7 4 2-6.4L3 9.2h6.6z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg></span>Add favourite</button></li>
</ul>
<button class="fb-08__fab" id="fb-08-fab" type="button" popovertarget="fb-08-dial" aria-label="Quick create">
<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-08" aria-label="Pure CSS speed dial demo">
<div class="fb-08__page">
<p class="fb-08__eyebrow">popover · anchor() · @starting-style</p>
<h2 class="fb-08__title">Top layer, light dismiss, zero script</h2>
<p class="fb-08__sub">Click the button, then click anywhere outside — the browser closes it, restores focus and keeps <code>aria-expanded</code> in sync on its own.</p>
<p class="fb-08__chip">popovertarget · :popover-open · allow-discrete</p>
</div>
<div class="fb-08__dock">
<ul class="fb-08__dial" id="fb-08-dial" popover>
<li><button class="fb-08__act" type="button"><span class="fb-08__ico" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></span>New task</button></li>
<li><button class="fb-08__act" type="button"><span class="fb-08__ico" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><rect x="4" y="5" width="16" height="15" rx="2.5" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M8 3v4M16 3v4M4 10h16" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg></span>Schedule</button></li>
<li><button class="fb-08__act" type="button"><span class="fb-08__ico" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path d="M4 6h16M4 12h16M4 18h10" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg></span>New list</button></li>
<li><button class="fb-08__act" type="button"><span class="fb-08__ico" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path d="M12 3l2.4 6.2H21l-5.3 3.9 2 6.4-5.7-4-5.7 4 2-6.4L3 9.2h6.6z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg></span>Add favourite</button></li>
</ul>
<button class="fb-08__fab" id="fb-08-fab" type="button" popovertarget="fb-08-dial" aria-label="Quick create">
<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-08 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--fb-08-bg);
--fb-08-bg: oklch(0.97 0.014 195);
--fb-08-brand: oklch(0.58 0.13 195);
--fb-08-brandDeep: oklch(0.44 0.12 200);
--fb-08-ink: oklch(0.24 0.03 200);
--fb-08-mut: oklch(0.53 0.02 200);
--fb-08-card: oklch(1 0 0);
font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
color: var(--fb-08-ink);
position: relative;
display: grid;
place-items: center;
padding: clamp(20px,5vw,56px);
}
.fb-08 *,
.fb-08 *::before,
.fb-08 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.fb-08__page {
width: min(100%,56ch);
display: grid;
gap: 12px;
justify-items: center;
text-align: center;
}
.fb-08__eyebrow {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--fb-08-brandDeep);
}
.fb-08__title {
font-size: clamp(26px,4.6vw,44px);
line-height: 1.05;
letter-spacing: -.03em;
font-weight: 700;
text-wrap: balance;
}
.fb-08__sub {
font-size: clamp(13.5px,1.7vw,16.5px);
line-height: 1.62;
color: var(--fb-08-mut);
text-wrap: pretty;
}
.fb-08__sub code {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: .86em;
background: oklch(0.58 0.13 195/.14);
padding: 2px 6px;
border-radius: 6px;
}
.fb-08__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11px;
color: var(--fb-08-mut);
padding: 8px 14px;
border: 1px solid oklch(0.24 0.03 200/.14);
border-radius: 99px;
}
.fb-08__dock {
position: absolute;
right: clamp(18px,4vw,40px);
bottom: calc(clamp(18px,4vw,40px) + env(safe-area-inset-bottom));
}
.fb-08__fab {
anchor-name: --fb-08-anchor;
display: grid;
place-items: center;
inline-size: 62px;
block-size: 62px;
border: 0;
border-radius: 20px;
cursor: pointer;
color: oklch(0.99 0.01 195);
background: linear-gradient(155deg,oklch(0.68 0.14 190),var(--fb-08-brandDeep));
box-shadow: 0 14px 32px -12px oklch(0.44 0.12 200/.9),0 2px 6px oklch(0.3 0.06 200/.28);
transition: transform .28s cubic-bezier(.2,1.3,.4,1),border-radius .28s ease;
}
.fb-08__fab svg {
width: 27px;
height: 27px;
transition: rotate .32s cubic-bezier(.3,1.35,.4,1);
}
.fb-08__fab:hover {
transform: translateY(-2px);
}
.fb-08__fab:active {
transform: scale(.94);
}
.fb-08__fab:focus-visible {
outline: 3px solid var(--fb-08-brandDeep);
outline-offset: 4px;
}
.fb-08__fab[aria-expanded="true"] {
border-radius: 50%;
}
.fb-08__fab[aria-expanded="true"] svg {
rotate: 135deg;
}
.fb-08__dial {
position: absolute;
margin: 0;
padding: 8px;
border: 1px solid oklch(0.24 0.03 200/.1);
border-radius: 20px;
background: var(--fb-08-card);
list-style: none;
display: grid;
gap: 4px;
min-width: 210px;
box-shadow: 0 26px 60px -24px oklch(0.24 0.03 200/.65),0 2px 6px oklch(0.24 0.03 200/.1);
inset: auto;
right: 0;
bottom: 74px;
opacity: 0;
translate: 0 14px;
scale: .96;
transform-origin: 100% 100%;
transition: opacity .26s ease,translate .3s cubic-bezier(.2,1.1,.3,1),scale .3s cubic-bezier(.2,1.1,.3,1),overlay .3s allow-discrete,display .3s allow-discrete;
}
.fb-08__dial:popover-open {
opacity: 1;
translate: 0 0;
scale: 1;
}
@starting-style {
.fb-08__dial:popover-open {
opacity: 0;
translate: 0 14px;
scale: .96;
}
}
@supports (anchor-name: --x) {
.fb-08__dial {
position-anchor: --fb-08-anchor;
right: anchor(right);
bottom: calc(anchor(top) + 12px);
position-try-fallbacks: flip-block;
}
}
.fb-08__act {
display: flex;
align-items: center;
gap: 11px;
width: 100%;
min-height: 46px;
padding: 0 16px 0 8px;
border: 0;
border-radius: 14px;
background: transparent;
color: var(--fb-08-ink);
font-size: 14px;
font-weight: 600;
text-align: left;
cursor: pointer;
transition: background .16s ease,transform .16s ease;
}
.fb-08__act:hover {
background: oklch(0.58 0.13 195/.1);
}
.fb-08__act:active {
transform: scale(.98);
}
.fb-08__act:focus-visible {
outline: 2.5px solid var(--fb-08-brandDeep);
outline-offset: -2px;
}
.fb-08__ico {
flex: none;
width: 34px;
height: 34px;
border-radius: 11px;
display: grid;
place-items: center;
background: oklch(0.58 0.13 195/.12);
color: var(--fb-08-brandDeep);
}
.fb-08__ico svg {
width: 19px;
height: 19px;
}
@media (prefers-reduced-motion: reduce) {
.fb-08__dial,
.fb-08__fab,
.fb-08__fab svg {
transition-duration: .01ms !important;
}
}.fb-08 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--fb-08-bg);
--fb-08-bg: oklch(0.97 0.014 195);
--fb-08-brand: oklch(0.58 0.13 195);
--fb-08-brandDeep: oklch(0.44 0.12 200);
--fb-08-ink: oklch(0.24 0.03 200);
--fb-08-mut: oklch(0.53 0.02 200);
--fb-08-card: oklch(1 0 0);
font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
color: var(--fb-08-ink);
position: relative;
display: grid;
place-items: center;
padding: clamp(20px,5vw,56px);
}
.fb-08 *,
.fb-08 *::before,
.fb-08 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.fb-08__page {
width: min(100%,56ch);
display: grid;
gap: 12px;
justify-items: center;
text-align: center;
}
.fb-08__eyebrow {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--fb-08-brandDeep);
}
.fb-08__title {
font-size: clamp(26px,4.6vw,44px);
line-height: 1.05;
letter-spacing: -.03em;
font-weight: 700;
text-wrap: balance;
}
.fb-08__sub {
font-size: clamp(13.5px,1.7vw,16.5px);
line-height: 1.62;
color: var(--fb-08-mut);
text-wrap: pretty;
}
.fb-08__sub code {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: .86em;
background: oklch(0.58 0.13 195/.14);
padding: 2px 6px;
border-radius: 6px;
}
.fb-08__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11px;
color: var(--fb-08-mut);
padding: 8px 14px;
border: 1px solid oklch(0.24 0.03 200/.14);
border-radius: 99px;
}
.fb-08__dock {
position: absolute;
right: clamp(18px,4vw,40px);
bottom: calc(clamp(18px,4vw,40px) + env(safe-area-inset-bottom));
}
.fb-08__fab {
anchor-name: --fb-08-anchor;
display: grid;
place-items: center;
inline-size: 62px;
block-size: 62px;
border: 0;
border-radius: 20px;
cursor: pointer;
color: oklch(0.99 0.01 195);
background: linear-gradient(155deg,oklch(0.68 0.14 190),var(--fb-08-brandDeep));
box-shadow: 0 14px 32px -12px oklch(0.44 0.12 200/.9),0 2px 6px oklch(0.3 0.06 200/.28);
transition: transform .28s cubic-bezier(.2,1.3,.4,1),border-radius .28s ease;
}
.fb-08__fab svg {
width: 27px;
height: 27px;
transition: rotate .32s cubic-bezier(.3,1.35,.4,1);
}
.fb-08__fab:hover {
transform: translateY(-2px);
}
.fb-08__fab:active {
transform: scale(.94);
}
.fb-08__fab:focus-visible {
outline: 3px solid var(--fb-08-brandDeep);
outline-offset: 4px;
}
.fb-08__fab[aria-expanded="true"] {
border-radius: 50%;
}
.fb-08__fab[aria-expanded="true"] svg {
rotate: 135deg;
}
.fb-08__dial {
position: absolute;
margin: 0;
padding: 8px;
border: 1px solid oklch(0.24 0.03 200/.1);
border-radius: 20px;
background: var(--fb-08-card);
list-style: none;
display: grid;
gap: 4px;
min-width: 210px;
box-shadow: 0 26px 60px -24px oklch(0.24 0.03 200/.65),0 2px 6px oklch(0.24 0.03 200/.1);
inset: auto;
right: 0;
bottom: 74px;
opacity: 0;
translate: 0 14px;
scale: .96;
transform-origin: 100% 100%;
transition: opacity .26s ease,translate .3s cubic-bezier(.2,1.1,.3,1),scale .3s cubic-bezier(.2,1.1,.3,1),overlay .3s allow-discrete,display .3s allow-discrete;
}
.fb-08__dial:popover-open {
opacity: 1;
translate: 0 0;
scale: 1;
}
@starting-style {
.fb-08__dial:popover-open {
opacity: 0;
translate: 0 14px;
scale: .96;
}
}
@supports (anchor-name: --x) {
.fb-08__dial {
position-anchor: --fb-08-anchor;
right: anchor(right);
bottom: calc(anchor(top) + 12px);
position-try-fallbacks: flip-block;
}
}
.fb-08__act {
display: flex;
align-items: center;
gap: 11px;
width: 100%;
min-height: 46px;
padding: 0 16px 0 8px;
border: 0;
border-radius: 14px;
background: transparent;
color: var(--fb-08-ink);
font-size: 14px;
font-weight: 600;
text-align: left;
cursor: pointer;
transition: background .16s ease,transform .16s ease;
}
.fb-08__act:hover {
background: oklch(0.58 0.13 195/.1);
}
.fb-08__act:active {
transform: scale(.98);
}
.fb-08__act:focus-visible {
outline: 2.5px solid var(--fb-08-brandDeep);
outline-offset: -2px;
}
.fb-08__ico {
flex: none;
width: 34px;
height: 34px;
border-radius: 11px;
display: grid;
place-items: center;
background: oklch(0.58 0.13 195/.12);
color: var(--fb-08-brandDeep);
}
.fb-08__ico svg {
width: 19px;
height: 19px;
}
@media (prefers-reduced-motion: reduce) {
.fb-08__dial,
.fb-08__fab,
.fb-08__fab svg {
transition-duration: .01ms !important;
}
}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: Pure CSS FAB Speed Dial (No JavaScript)
Source: https://codefronts.com/components/css-floating-buttons/pure-css-fab-speed-dial-no-javascript/
A speed dial with a real open/close state, light dismiss, Escape handling and top-layer stacking — and not a line of script. The popover attribute gives you the behaviour for free, CSS anchor positioning places the menu against the button, and @starting-style animates it in from display:none. This is the pattern that finally makes the old checkbox hack obsolete.
## HTML
```html
<section class="fb-08" aria-label="Pure CSS speed dial demo">
<div class="fb-08__page">
<p class="fb-08__eyebrow">popover · anchor() · @starting-style</p>
<h2 class="fb-08__title">Top layer, light dismiss, zero script</h2>
<p class="fb-08__sub">Click the button, then click anywhere outside — the browser closes it, restores focus and keeps <code>aria-expanded</code> in sync on its own.</p>
<p class="fb-08__chip">popovertarget · :popover-open · allow-discrete</p>
</div>
<div class="fb-08__dock">
<ul class="fb-08__dial" id="fb-08-dial" popover>
<li><button class="fb-08__act" type="button"><span class="fb-08__ico" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></span>New task</button></li>
<li><button class="fb-08__act" type="button"><span class="fb-08__ico" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><rect x="4" y="5" width="16" height="15" rx="2.5" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M8 3v4M16 3v4M4 10h16" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg></span>Schedule</button></li>
<li><button class="fb-08__act" type="button"><span class="fb-08__ico" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path d="M4 6h16M4 12h16M4 18h10" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg></span>New list</button></li>
<li><button class="fb-08__act" type="button"><span class="fb-08__ico" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path d="M12 3l2.4 6.2H21l-5.3 3.9 2 6.4-5.7-4-5.7 4 2-6.4L3 9.2h6.6z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg></span>Add favourite</button></li>
</ul>
<button class="fb-08__fab" id="fb-08-fab" type="button" popovertarget="fb-08-dial" aria-label="Quick create">
<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-08 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--fb-08-bg);
--fb-08-bg: oklch(0.97 0.014 195);
--fb-08-brand: oklch(0.58 0.13 195);
--fb-08-brandDeep: oklch(0.44 0.12 200);
--fb-08-ink: oklch(0.24 0.03 200);
--fb-08-mut: oklch(0.53 0.02 200);
--fb-08-card: oklch(1 0 0);
font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
color: var(--fb-08-ink);
position: relative;
display: grid;
place-items: center;
padding: clamp(20px,5vw,56px);
}
.fb-08 *,
.fb-08 *::before,
.fb-08 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.fb-08__page {
width: min(100%,56ch);
display: grid;
gap: 12px;
justify-items: center;
text-align: center;
}
.fb-08__eyebrow {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--fb-08-brandDeep);
}
.fb-08__title {
font-size: clamp(26px,4.6vw,44px);
line-height: 1.05;
letter-spacing: -.03em;
font-weight: 700;
text-wrap: balance;
}
.fb-08__sub {
font-size: clamp(13.5px,1.7vw,16.5px);
line-height: 1.62;
color: var(--fb-08-mut);
text-wrap: pretty;
}
.fb-08__sub code {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: .86em;
background: oklch(0.58 0.13 195/.14);
padding: 2px 6px;
border-radius: 6px;
}
.fb-08__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11px;
color: var(--fb-08-mut);
padding: 8px 14px;
border: 1px solid oklch(0.24 0.03 200/.14);
border-radius: 99px;
}
.fb-08__dock {
position: absolute;
right: clamp(18px,4vw,40px);
bottom: calc(clamp(18px,4vw,40px) + env(safe-area-inset-bottom));
}
.fb-08__fab {
anchor-name: --fb-08-anchor;
display: grid;
place-items: center;
inline-size: 62px;
block-size: 62px;
border: 0;
border-radius: 20px;
cursor: pointer;
color: oklch(0.99 0.01 195);
background: linear-gradient(155deg,oklch(0.68 0.14 190),var(--fb-08-brandDeep));
box-shadow: 0 14px 32px -12px oklch(0.44 0.12 200/.9),0 2px 6px oklch(0.3 0.06 200/.28);
transition: transform .28s cubic-bezier(.2,1.3,.4,1),border-radius .28s ease;
}
.fb-08__fab svg {
width: 27px;
height: 27px;
transition: rotate .32s cubic-bezier(.3,1.35,.4,1);
}
.fb-08__fab:hover {
transform: translateY(-2px);
}
.fb-08__fab:active {
transform: scale(.94);
}
.fb-08__fab:focus-visible {
outline: 3px solid var(--fb-08-brandDeep);
outline-offset: 4px;
}
.fb-08__fab[aria-expanded="true"] {
border-radius: 50%;
}
.fb-08__fab[aria-expanded="true"] svg {
rotate: 135deg;
}
.fb-08__dial {
position: absolute;
margin: 0;
padding: 8px;
border: 1px solid oklch(0.24 0.03 200/.1);
border-radius: 20px;
background: var(--fb-08-card);
list-style: none;
display: grid;
gap: 4px;
min-width: 210px;
box-shadow: 0 26px 60px -24px oklch(0.24 0.03 200/.65),0 2px 6px oklch(0.24 0.03 200/.1);
inset: auto;
right: 0;
bottom: 74px;
opacity: 0;
translate: 0 14px;
scale: .96;
transform-origin: 100% 100%;
transition: opacity .26s ease,translate .3s cubic-bezier(.2,1.1,.3,1),scale .3s cubic-bezier(.2,1.1,.3,1),overlay .3s allow-discrete,display .3s allow-discrete;
}
.fb-08__dial:popover-open {
opacity: 1;
translate: 0 0;
scale: 1;
}
@starting-style {
.fb-08__dial:popover-open {
opacity: 0;
translate: 0 14px;
scale: .96;
}
}
@supports (anchor-name: --x) {
.fb-08__dial {
position-anchor: --fb-08-anchor;
right: anchor(right);
bottom: calc(anchor(top) + 12px);
position-try-fallbacks: flip-block;
}
}
.fb-08__act {
display: flex;
align-items: center;
gap: 11px;
width: 100%;
min-height: 46px;
padding: 0 16px 0 8px;
border: 0;
border-radius: 14px;
background: transparent;
color: var(--fb-08-ink);
font-size: 14px;
font-weight: 600;
text-align: left;
cursor: pointer;
transition: background .16s ease,transform .16s ease;
}
.fb-08__act:hover {
background: oklch(0.58 0.13 195/.1);
}
.fb-08__act:active {
transform: scale(.98);
}
.fb-08__act:focus-visible {
outline: 2.5px solid var(--fb-08-brandDeep);
outline-offset: -2px;
}
.fb-08__ico {
flex: none;
width: 34px;
height: 34px;
border-radius: 11px;
display: grid;
place-items: center;
background: oklch(0.58 0.13 195/.12);
color: var(--fb-08-brandDeep);
}
.fb-08__ico svg {
width: 19px;
height: 19px;
}
@media (prefers-reduced-motion: reduce) {
.fb-08__dial,
.fb-08__fab,
.fb-08__fab svg {
transition-duration: .01ms !important;
}
}
```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: Pure CSS FAB Speed Dial (No JavaScript)
Source: https://codefronts.com/components/css-floating-buttons/pure-css-fab-speed-dial-no-javascript/
A speed dial with a real open/close state, light dismiss, Escape handling and top-layer stacking — and not a line of script. The popover attribute gives you the behaviour for free, CSS anchor positioning places the menu against the button, and @starting-style animates it in from display:none. This is the pattern that finally makes the old checkbox hack obsolete.
## HTML
```html
<section class="fb-08" aria-label="Pure CSS speed dial demo">
<div class="fb-08__page">
<p class="fb-08__eyebrow">popover · anchor() · @starting-style</p>
<h2 class="fb-08__title">Top layer, light dismiss, zero script</h2>
<p class="fb-08__sub">Click the button, then click anywhere outside — the browser closes it, restores focus and keeps <code>aria-expanded</code> in sync on its own.</p>
<p class="fb-08__chip">popovertarget · :popover-open · allow-discrete</p>
</div>
<div class="fb-08__dock">
<ul class="fb-08__dial" id="fb-08-dial" popover>
<li><button class="fb-08__act" type="button"><span class="fb-08__ico" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></span>New task</button></li>
<li><button class="fb-08__act" type="button"><span class="fb-08__ico" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><rect x="4" y="5" width="16" height="15" rx="2.5" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M8 3v4M16 3v4M4 10h16" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg></span>Schedule</button></li>
<li><button class="fb-08__act" type="button"><span class="fb-08__ico" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path d="M4 6h16M4 12h16M4 18h10" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg></span>New list</button></li>
<li><button class="fb-08__act" type="button"><span class="fb-08__ico" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path d="M12 3l2.4 6.2H21l-5.3 3.9 2 6.4-5.7-4-5.7 4 2-6.4L3 9.2h6.6z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg></span>Add favourite</button></li>
</ul>
<button class="fb-08__fab" id="fb-08-fab" type="button" popovertarget="fb-08-dial" aria-label="Quick create">
<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-08 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--fb-08-bg);
--fb-08-bg: oklch(0.97 0.014 195);
--fb-08-brand: oklch(0.58 0.13 195);
--fb-08-brandDeep: oklch(0.44 0.12 200);
--fb-08-ink: oklch(0.24 0.03 200);
--fb-08-mut: oklch(0.53 0.02 200);
--fb-08-card: oklch(1 0 0);
font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
color: var(--fb-08-ink);
position: relative;
display: grid;
place-items: center;
padding: clamp(20px,5vw,56px);
}
.fb-08 *,
.fb-08 *::before,
.fb-08 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.fb-08__page {
width: min(100%,56ch);
display: grid;
gap: 12px;
justify-items: center;
text-align: center;
}
.fb-08__eyebrow {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--fb-08-brandDeep);
}
.fb-08__title {
font-size: clamp(26px,4.6vw,44px);
line-height: 1.05;
letter-spacing: -.03em;
font-weight: 700;
text-wrap: balance;
}
.fb-08__sub {
font-size: clamp(13.5px,1.7vw,16.5px);
line-height: 1.62;
color: var(--fb-08-mut);
text-wrap: pretty;
}
.fb-08__sub code {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: .86em;
background: oklch(0.58 0.13 195/.14);
padding: 2px 6px;
border-radius: 6px;
}
.fb-08__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11px;
color: var(--fb-08-mut);
padding: 8px 14px;
border: 1px solid oklch(0.24 0.03 200/.14);
border-radius: 99px;
}
.fb-08__dock {
position: absolute;
right: clamp(18px,4vw,40px);
bottom: calc(clamp(18px,4vw,40px) + env(safe-area-inset-bottom));
}
.fb-08__fab {
anchor-name: --fb-08-anchor;
display: grid;
place-items: center;
inline-size: 62px;
block-size: 62px;
border: 0;
border-radius: 20px;
cursor: pointer;
color: oklch(0.99 0.01 195);
background: linear-gradient(155deg,oklch(0.68 0.14 190),var(--fb-08-brandDeep));
box-shadow: 0 14px 32px -12px oklch(0.44 0.12 200/.9),0 2px 6px oklch(0.3 0.06 200/.28);
transition: transform .28s cubic-bezier(.2,1.3,.4,1),border-radius .28s ease;
}
.fb-08__fab svg {
width: 27px;
height: 27px;
transition: rotate .32s cubic-bezier(.3,1.35,.4,1);
}
.fb-08__fab:hover {
transform: translateY(-2px);
}
.fb-08__fab:active {
transform: scale(.94);
}
.fb-08__fab:focus-visible {
outline: 3px solid var(--fb-08-brandDeep);
outline-offset: 4px;
}
.fb-08__fab[aria-expanded="true"] {
border-radius: 50%;
}
.fb-08__fab[aria-expanded="true"] svg {
rotate: 135deg;
}
.fb-08__dial {
position: absolute;
margin: 0;
padding: 8px;
border: 1px solid oklch(0.24 0.03 200/.1);
border-radius: 20px;
background: var(--fb-08-card);
list-style: none;
display: grid;
gap: 4px;
min-width: 210px;
box-shadow: 0 26px 60px -24px oklch(0.24 0.03 200/.65),0 2px 6px oklch(0.24 0.03 200/.1);
inset: auto;
right: 0;
bottom: 74px;
opacity: 0;
translate: 0 14px;
scale: .96;
transform-origin: 100% 100%;
transition: opacity .26s ease,translate .3s cubic-bezier(.2,1.1,.3,1),scale .3s cubic-bezier(.2,1.1,.3,1),overlay .3s allow-discrete,display .3s allow-discrete;
}
.fb-08__dial:popover-open {
opacity: 1;
translate: 0 0;
scale: 1;
}
@starting-style {
.fb-08__dial:popover-open {
opacity: 0;
translate: 0 14px;
scale: .96;
}
}
@supports (anchor-name: --x) {
.fb-08__dial {
position-anchor: --fb-08-anchor;
right: anchor(right);
bottom: calc(anchor(top) + 12px);
position-try-fallbacks: flip-block;
}
}
.fb-08__act {
display: flex;
align-items: center;
gap: 11px;
width: 100%;
min-height: 46px;
padding: 0 16px 0 8px;
border: 0;
border-radius: 14px;
background: transparent;
color: var(--fb-08-ink);
font-size: 14px;
font-weight: 600;
text-align: left;
cursor: pointer;
transition: background .16s ease,transform .16s ease;
}
.fb-08__act:hover {
background: oklch(0.58 0.13 195/.1);
}
.fb-08__act:active {
transform: scale(.98);
}
.fb-08__act:focus-visible {
outline: 2.5px solid var(--fb-08-brandDeep);
outline-offset: -2px;
}
.fb-08__ico {
flex: none;
width: 34px;
height: 34px;
border-radius: 11px;
display: grid;
place-items: center;
background: oklch(0.58 0.13 195/.12);
color: var(--fb-08-brandDeep);
}
.fb-08__ico svg {
width: 19px;
height: 19px;
}
@media (prefers-reduced-motion: reduce) {
.fb-08__dial,
.fb-08__fab,
.fb-08__fab svg {
transition-duration: .01ms !important;
}
}
```How this works
Three attributes do what used to take a small library:
<button popovertarget="dial" style="anchor-name:--fab">+</button>
<ul id="dial" popover style="position-anchor:--fab">…</ul>[popover]{ position:absolute; inset:auto;
bottom:anchor(--fab top); right:anchor(--fab right);
opacity:0; translate:0 12px;
transition:opacity .26s, translate .26s, overlay .26s allow-discrete, display .26s allow-discrete }
[popover]:popover-open{ opacity:1; translate:0 0 }
@starting-style{ [popover]:popover-open{ opacity:0; translate:0 12px } }The browser handles the rest: the popover renders in the top layer (so it can never be clipped by an ancestor's overflow or lose a z-index war), clicking outside dismisses it, Escape closes it, and focus returns to the invoker. aria-expanded is even maintained by the browser on the invoking button.
@starting-style is what makes it animate: an element going from display:none normally has no "before" state to transition from. The rule supplies one. transition-behavior:allow-discrete on display and overlay keeps the element visible for the duration of the exit animation.
Anchor positioning removes the last reason to reach for JS: bottom:anchor(--fab top) means "my bottom edge sits on the button's top edge", recomputed automatically on scroll and resize.
Make it yours
- Fallback placement:
position-try-fallbacks: flip-block, flip-inlinelets the dial flip above/below or left/right when it would overflow the viewport. - Direction: change the two anchor() edges —
left:anchor(--fab right)gives a horizontal dial with the same markup. - No-anchor fallback: wrap the anchor rules in
@supports (anchor-name:--x)and ship plain absolute positioning otherwise, as this demo does. - Hover-only hint: a
popover="hint"popover shows on hover without stealing focus — useful for tooltips beside each action.
Gotchas — read before shipping
- Without
transition-behavior:allow-discrete(or theoverlaytransition) the exit animation is skipped entirely — the popover just disappears. - @starting-style rules must come after the base rule, and specificity still applies. A more specific later rule silently wins.
- Anchor positioning needs the anchor to be in the same top-layer-friendly containing block; an
overflow:hiddenancestor no longer clips a popover, but it can still clip a non-popover anchored element. - popover="auto" closes other open auto popovers. If you need two dials open at once, use popover="manual" and handle dismissal yourself.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 125+ | 26+ | 131+ | 125+ |
Popover API: Chrome 114+, Safari 17+, Firefox 125+. @starting-style: Chrome 117+, Safari 17.5+, Firefox 129+. CSS anchor positioning: Chrome 125+, Safari 26+, Firefox 131+ — this demo wraps anchor rules in @supports and falls back to absolute positioning, so it works everywhere the popover API exists.