29 / 34
All 34 in 34 CSS Floating Buttons
- 01Material Design 3 Floating Action Button (FAB)
- 02iOS-Style Compose Floating Button
- 03WhatsApp Floating Message Button CSS
- 04Intercom / Drift Chat Bubble Floating Button
- 05Accessible WAI-ARIA Floating Action Button (WCAG 2.2)
- 06Extended FAB with Icon and Label (Material Design 3)
- 07Speed Dial Radial Menu FAB with Backdrop
- 08Pure CSS FAB Speed Dial (No JavaScript)
- 09Material Gradient Floating Action Button CSS
- 10Vertical and Horizontal Speed Dial FAB Menu CSS
- 11Morphing FAB Button That Expands to Pill CSS
- 12Scroll-to-Top Floating Button with SVG Progress Ring
- 13Floating Chat Widget FAB with Notification Panel
- 14Fan and Arc Radial Spread FAB Menu CSS
- 15Apple Squircle Superellipse Floating Button CSS
- 16Social Share Floating Button Cluster CSS
- 17Glassmorphism Floating Action Button with Backdrop Blur
- 18Cyberpunk Neon Glow Floating Button CSS
- 19Floating Compose Button with Tooltip on Hover CSS
- 20Draggable Floating Action Button with Corner Snap CSS
- 21Notification Bell Floating Button with Badge Counter
- 22Floating Cart Button with Live Badge Counter CSS
- 23Magnetic Cursor Follower Floating Button CSS
- 24Dark Mode Theme Toggle Floating Button CSS
- 25Classic Gradient Circle Plus FAB CSS
- 26Pulse Ring Attention-Getter Floating Button CSS
- 27Pill-Shaped FAB with Icon and Text Label CSS
- 28Neo-Brutalist Hard Shadow Floating Button CSS
- 29Floating Button with Loading Spinner State CSS
- 30Draggable Floating Button with 6-Dot Grip Handle CSS
- 31Voice Recording Microphone Floating Button CSS
- 32Aurora Gradient Drift Premium Floating Button CSS
- 33Quick Emoji Reply Reaction Floating Button CSS
- 34Modern Squircle Rounded Square FAB CSS
Floating Button with Loading Spinner State CSS
The three-state async button — idle, pending, done — with the transitions between them treated as seriously as the states themselves. The spinner uses an animated dash gap so it never appears to stall, the success check draws in with stroke-dashoffset, and the whole thing is announced through aria-busy and a status region rather than by colour alone.
Nucleo: 44,000+ SVG icons in five families · All Icons Bundle $149
Nucleo · Affiliate link
The code
<section class="fb-29" aria-label="Floating button with loading state demo">
<div class="fb-29__stage">
<p class="fb-29__eyebrow">idle → loading → done → idle</p>
<h2 class="fb-29__title">Three states, one attribute</h2>
<p class="fb-29__sub">Click any button. The dash gap breathes so the spinner never looks frozen, the check draws in over 420ms, and it all resets after a beat.</p>
<div class="fb-29__row">
<button class="fb-29__fab" id="fb-29-a" type="button" data-state="idle" aria-label="Save document">
<span class="fb-29__ico" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path d="M5 4h11l3 3v13H5z" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linejoin="round"/><path d="M8 4v5h7M8 20v-6h8v6" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linejoin="round"/></svg></span>
<span class="fb-29__spin" aria-hidden="true"><svg viewBox="0 0 44 44" focusable="false"><circle cx="22" cy="22" r="17" fill="none" stroke="currentColor" stroke-width="3.4" stroke-linecap="round"/></svg></span>
<span class="fb-29__ok" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path class="fb-29__check" d="M5 12.6l4.6 4.6L19 7.4" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
</button>
<button class="fb-29__pill" id="fb-29-b" type="button" data-state="idle">
<span class="fb-29__iconstack" aria-hidden="true">
<span class="fb-29__ico"><svg viewBox="0 0 24 24" focusable="false"><path d="M12 17V6M8 10l4-4 4 4M5 19h14" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
<span class="fb-29__spin"><svg viewBox="0 0 44 44" focusable="false"><circle cx="22" cy="22" r="17" fill="none" stroke="currentColor" stroke-width="3.4" stroke-linecap="round"/></svg></span>
<span class="fb-29__ok"><svg viewBox="0 0 24 24" focusable="false"><path class="fb-29__check" d="M5 12.6l4.6 4.6L19 7.4" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
</span>
<b class="fb-29__label" id="fb-29-blabel">Publish</b>
</button>
</div>
<p class="fb-29__live" id="fb-29-live" role="status" aria-live="polite">Ready</p>
<p class="fb-29__chip">aria-busy · 400ms spinner floor · auto-reset 1.8s</p>
</div>
</section><section class="fb-29" aria-label="Floating button with loading state demo">
<div class="fb-29__stage">
<p class="fb-29__eyebrow">idle → loading → done → idle</p>
<h2 class="fb-29__title">Three states, one attribute</h2>
<p class="fb-29__sub">Click any button. The dash gap breathes so the spinner never looks frozen, the check draws in over 420ms, and it all resets after a beat.</p>
<div class="fb-29__row">
<button class="fb-29__fab" id="fb-29-a" type="button" data-state="idle" aria-label="Save document">
<span class="fb-29__ico" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path d="M5 4h11l3 3v13H5z" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linejoin="round"/><path d="M8 4v5h7M8 20v-6h8v6" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linejoin="round"/></svg></span>
<span class="fb-29__spin" aria-hidden="true"><svg viewBox="0 0 44 44" focusable="false"><circle cx="22" cy="22" r="17" fill="none" stroke="currentColor" stroke-width="3.4" stroke-linecap="round"/></svg></span>
<span class="fb-29__ok" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path class="fb-29__check" d="M5 12.6l4.6 4.6L19 7.4" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
</button>
<button class="fb-29__pill" id="fb-29-b" type="button" data-state="idle">
<span class="fb-29__iconstack" aria-hidden="true">
<span class="fb-29__ico"><svg viewBox="0 0 24 24" focusable="false"><path d="M12 17V6M8 10l4-4 4 4M5 19h14" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
<span class="fb-29__spin"><svg viewBox="0 0 44 44" focusable="false"><circle cx="22" cy="22" r="17" fill="none" stroke="currentColor" stroke-width="3.4" stroke-linecap="round"/></svg></span>
<span class="fb-29__ok"><svg viewBox="0 0 24 24" focusable="false"><path class="fb-29__check" d="M5 12.6l4.6 4.6L19 7.4" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
</span>
<b class="fb-29__label" id="fb-29-blabel">Publish</b>
</button>
</div>
<p class="fb-29__live" id="fb-29-live" role="status" aria-live="polite">Ready</p>
<p class="fb-29__chip">aria-busy · 400ms spinner floor · auto-reset 1.8s</p>
</div>
</section>.fb-29 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--fb-29-bg);
--fb-29-bg: linear-gradient(180deg,oklch(0.98 0.008 265),oklch(0.94 0.02 258));
--fb-29-brand: oklch(0.52 0.17 265);
--fb-29-brandDeep: oklch(0.38 0.15 268);
--fb-29-ok: oklch(0.62 0.16 148);
--fb-29-ink: oklch(0.22 0.025 265);
--fb-29-mut: oklch(0.53 0.015 265);
font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
color: var(--fb-29-ink);
display: grid;
place-items: center;
padding: clamp(20px,5vw,56px);
}
.fb-29 *,
.fb-29 *::before,
.fb-29 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.fb-29__stage {
width: min(100%,56ch);
display: grid;
gap: clamp(16px,3vw,26px);
justify-items: center;
text-align: center;
}
.fb-29__eyebrow {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--fb-29-brand);
}
.fb-29__title {
font-size: clamp(26px,4.6vw,44px);
line-height: 1.05;
letter-spacing: -.03em;
font-weight: 700;
text-wrap: balance;
}
.fb-29__sub {
max-width: 50ch;
font-size: clamp(13.5px,1.7vw,16.5px);
line-height: 1.62;
color: var(--fb-29-mut);
text-wrap: pretty;
}
.fb-29__row {
display: flex;
flex-wrap: wrap;
gap: 22px;
justify-content: center;
align-items: center;
padding: 8px 0;
}
.fb-29__fab,
.fb-29__pill {
position: relative;
border: 0;
border-radius: 22px;
cursor: pointer;
color: oklch(0.99 0.01 265);
background: linear-gradient(150deg,oklch(0.6 0.18 262),var(--fb-29-brandDeep));
box-shadow: 0 14px 32px -12px oklch(0.38 0.15 268/.85),0 2px 6px oklch(0.3 0.07 268/.28);
transition: transform .26s cubic-bezier(.2,1.3,.4,1),background .3s ease,box-shadow .3s ease;
}
.fb-29__fab {
display: grid;
place-items: center;
inline-size: 64px;
block-size: 64px;
}
.fb-29__pill {
display: inline-flex;
align-items: center;
gap: 11px;
block-size: 64px;
padding-inline: 22px 26px;
border-radius: 99px;
font-family: inherit;
font-size: 16px;
font-weight: 640;
}
.fb-29__pill .fb-29__iconstack {
position: relative;
display: grid;
place-items: center;
inline-size: 30px;
block-size: 30px;
flex: none;
}
.fb-29__fab:hover,
.fb-29__pill:hover {
transform: translateY(-2px);
}
.fb-29__fab:active,
.fb-29__pill:active {
transform: scale(.96);
}
.fb-29__fab:focus-visible,
.fb-29__pill:focus-visible {
outline: 3px solid var(--fb-29-brandDeep);
outline-offset: 4px;
}
.fb-29__fab>.fb-29__ico,
.fb-29__fab>.fb-29__spin,
.fb-29__fab>.fb-29__ok,
.fb-29__pill .fb-29__iconstack>.fb-29__ico,
.fb-29__pill .fb-29__iconstack>.fb-29__spin,
.fb-29__pill .fb-29__iconstack>.fb-29__ok {
grid-area: 1/1;
display: grid;
place-items: center;
transition: opacity .2s ease,scale .28s cubic-bezier(.2,1.4,.4,1);
}
.fb-29__ico svg,
.fb-29__ok svg {
width: 26px;
height: 26px;
}
.fb-29__spin svg {
width: 30px;
height: 30px;
animation: fb-29-rot 1.1s linear infinite;
}
.fb-29__spin circle {
stroke-dasharray: 22 90;
animation: fb-29-dash 1.5s ease-in-out infinite;
}
@keyframes fb-29-rot {
to {
rotate: 360deg;
}
}
@keyframes fb-29-dash {
0% {
stroke-dasharray: 12 96;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 62 46;
stroke-dashoffset: -16;
}
100% {
stroke-dasharray: 12 96;
stroke-dashoffset: -104;
}
}
.fb-29__spin,
.fb-29__ok {
opacity: 0;
scale: .6;
pointer-events: none;
}
.fb-29__check {
stroke-dasharray: var(--fb-29-len,32);
stroke-dashoffset: var(--fb-29-len,32);
}
[data-state="loading"] .fb-29__ico {
opacity: 0;
scale: .6;
}
[data-state="loading"] .fb-29__spin {
opacity: 1;
scale: 1;
}
[data-state="done"] .fb-29__ico {
opacity: 0;
scale: .6;
}
[data-state="done"] .fb-29__ok {
opacity: 1;
scale: 1;
}
[data-state="done"] .fb-29__check {
animation: fb-29-draw .42s cubic-bezier(.2,.9,.3,1) forwards;
}
@keyframes fb-29-draw {
to {
stroke-dashoffset: 0;
}
}
.fb-29__fab[data-state="done"],
.fb-29__pill[data-state="done"] {
background: linear-gradient(150deg,oklch(0.7 0.16 150),oklch(0.5 0.14 150));
box-shadow: 0 14px 32px -12px oklch(0.5 0.14 150/.8);
}
.fb-29__fab[data-state="loading"],
.fb-29__pill[data-state="loading"] {
cursor: progress;
}
.fb-29__label {
font-weight: 640;
letter-spacing: -.01em;
}
.fb-29__live {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 12px;
color: var(--fb-29-brandDeep);
background: oklch(0.52 0.17 265/.09);
border-radius: 10px;
padding: 8px 15px;
min-width: 12ch;
}
.fb-29__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11px;
color: var(--fb-29-mut);
padding: 8px 14px;
border: 1px solid oklch(0.22 0.025 265/.14);
border-radius: 99px;
}
@media (prefers-reduced-motion: reduce) {
.fb-29 * {
animation-duration: .01ms !important;
transition: none !important;
}
.fb-29__check {
stroke-dashoffset: 0;
}
}.fb-29 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--fb-29-bg);
--fb-29-bg: linear-gradient(180deg,oklch(0.98 0.008 265),oklch(0.94 0.02 258));
--fb-29-brand: oklch(0.52 0.17 265);
--fb-29-brandDeep: oklch(0.38 0.15 268);
--fb-29-ok: oklch(0.62 0.16 148);
--fb-29-ink: oklch(0.22 0.025 265);
--fb-29-mut: oklch(0.53 0.015 265);
font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
color: var(--fb-29-ink);
display: grid;
place-items: center;
padding: clamp(20px,5vw,56px);
}
.fb-29 *,
.fb-29 *::before,
.fb-29 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.fb-29__stage {
width: min(100%,56ch);
display: grid;
gap: clamp(16px,3vw,26px);
justify-items: center;
text-align: center;
}
.fb-29__eyebrow {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--fb-29-brand);
}
.fb-29__title {
font-size: clamp(26px,4.6vw,44px);
line-height: 1.05;
letter-spacing: -.03em;
font-weight: 700;
text-wrap: balance;
}
.fb-29__sub {
max-width: 50ch;
font-size: clamp(13.5px,1.7vw,16.5px);
line-height: 1.62;
color: var(--fb-29-mut);
text-wrap: pretty;
}
.fb-29__row {
display: flex;
flex-wrap: wrap;
gap: 22px;
justify-content: center;
align-items: center;
padding: 8px 0;
}
.fb-29__fab,
.fb-29__pill {
position: relative;
border: 0;
border-radius: 22px;
cursor: pointer;
color: oklch(0.99 0.01 265);
background: linear-gradient(150deg,oklch(0.6 0.18 262),var(--fb-29-brandDeep));
box-shadow: 0 14px 32px -12px oklch(0.38 0.15 268/.85),0 2px 6px oklch(0.3 0.07 268/.28);
transition: transform .26s cubic-bezier(.2,1.3,.4,1),background .3s ease,box-shadow .3s ease;
}
.fb-29__fab {
display: grid;
place-items: center;
inline-size: 64px;
block-size: 64px;
}
.fb-29__pill {
display: inline-flex;
align-items: center;
gap: 11px;
block-size: 64px;
padding-inline: 22px 26px;
border-radius: 99px;
font-family: inherit;
font-size: 16px;
font-weight: 640;
}
.fb-29__pill .fb-29__iconstack {
position: relative;
display: grid;
place-items: center;
inline-size: 30px;
block-size: 30px;
flex: none;
}
.fb-29__fab:hover,
.fb-29__pill:hover {
transform: translateY(-2px);
}
.fb-29__fab:active,
.fb-29__pill:active {
transform: scale(.96);
}
.fb-29__fab:focus-visible,
.fb-29__pill:focus-visible {
outline: 3px solid var(--fb-29-brandDeep);
outline-offset: 4px;
}
.fb-29__fab>.fb-29__ico,
.fb-29__fab>.fb-29__spin,
.fb-29__fab>.fb-29__ok,
.fb-29__pill .fb-29__iconstack>.fb-29__ico,
.fb-29__pill .fb-29__iconstack>.fb-29__spin,
.fb-29__pill .fb-29__iconstack>.fb-29__ok {
grid-area: 1/1;
display: grid;
place-items: center;
transition: opacity .2s ease,scale .28s cubic-bezier(.2,1.4,.4,1);
}
.fb-29__ico svg,
.fb-29__ok svg {
width: 26px;
height: 26px;
}
.fb-29__spin svg {
width: 30px;
height: 30px;
animation: fb-29-rot 1.1s linear infinite;
}
.fb-29__spin circle {
stroke-dasharray: 22 90;
animation: fb-29-dash 1.5s ease-in-out infinite;
}
@keyframes fb-29-rot {
to {
rotate: 360deg;
}
}
@keyframes fb-29-dash {
0% {
stroke-dasharray: 12 96;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 62 46;
stroke-dashoffset: -16;
}
100% {
stroke-dasharray: 12 96;
stroke-dashoffset: -104;
}
}
.fb-29__spin,
.fb-29__ok {
opacity: 0;
scale: .6;
pointer-events: none;
}
.fb-29__check {
stroke-dasharray: var(--fb-29-len,32);
stroke-dashoffset: var(--fb-29-len,32);
}
[data-state="loading"] .fb-29__ico {
opacity: 0;
scale: .6;
}
[data-state="loading"] .fb-29__spin {
opacity: 1;
scale: 1;
}
[data-state="done"] .fb-29__ico {
opacity: 0;
scale: .6;
}
[data-state="done"] .fb-29__ok {
opacity: 1;
scale: 1;
}
[data-state="done"] .fb-29__check {
animation: fb-29-draw .42s cubic-bezier(.2,.9,.3,1) forwards;
}
@keyframes fb-29-draw {
to {
stroke-dashoffset: 0;
}
}
.fb-29__fab[data-state="done"],
.fb-29__pill[data-state="done"] {
background: linear-gradient(150deg,oklch(0.7 0.16 150),oklch(0.5 0.14 150));
box-shadow: 0 14px 32px -12px oklch(0.5 0.14 150/.8);
}
.fb-29__fab[data-state="loading"],
.fb-29__pill[data-state="loading"] {
cursor: progress;
}
.fb-29__label {
font-weight: 640;
letter-spacing: -.01em;
}
.fb-29__live {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 12px;
color: var(--fb-29-brandDeep);
background: oklch(0.52 0.17 265/.09);
border-radius: 10px;
padding: 8px 15px;
min-width: 12ch;
}
.fb-29__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11px;
color: var(--fb-29-mut);
padding: 8px 14px;
border: 1px solid oklch(0.22 0.025 265/.14);
border-radius: 99px;
}
@media (prefers-reduced-motion: reduce) {
.fb-29 * {
animation-duration: .01ms !important;
transition: none !important;
}
.fb-29__check {
stroke-dashoffset: 0;
}
}(() => {
const live = document.getElementById('fb-29-live');
const label = document.getElementById('fb-29-blabel');
const buttons = [document.getElementById('fb-29-a'), document.getElementById('fb-29-b')].filter(Boolean);
if (!buttons.length || buttons[0].dataset.fbWired) return;
buttons[0].dataset.fbWired = '1';
document.querySelectorAll('.fb-29__check').forEach((p) => {
const len = Math.ceil(p.getTotalLength() || 32);
p.style.setProperty('--fb-29-len', len);
});
buttons.forEach((btn) => {
btn.addEventListener('click', () => {
if (btn.dataset.state !== 'idle') return;
btn.dataset.state = 'loading';
btn.setAttribute('aria-busy', 'true');
btn.disabled = true;
if (label && btn.contains(label)) label.textContent = 'Publishing…';
live.textContent = 'Working…';
setTimeout(() => {
btn.dataset.state = 'done';
btn.setAttribute('aria-busy', 'false');
if (label && btn.contains(label)) label.textContent = 'Published';
live.textContent = 'Done — saved just now';
setTimeout(() => {
btn.dataset.state = 'idle';
btn.disabled = false;
if (label && btn.contains(label)) label.textContent = 'Publish';
live.textContent = 'Ready';
}, 1800);
}, 1500);
});
});
})();(() => {
const live = document.getElementById('fb-29-live');
const label = document.getElementById('fb-29-blabel');
const buttons = [document.getElementById('fb-29-a'), document.getElementById('fb-29-b')].filter(Boolean);
if (!buttons.length || buttons[0].dataset.fbWired) return;
buttons[0].dataset.fbWired = '1';
document.querySelectorAll('.fb-29__check').forEach((p) => {
const len = Math.ceil(p.getTotalLength() || 32);
p.style.setProperty('--fb-29-len', len);
});
buttons.forEach((btn) => {
btn.addEventListener('click', () => {
if (btn.dataset.state !== 'idle') return;
btn.dataset.state = 'loading';
btn.setAttribute('aria-busy', 'true');
btn.disabled = true;
if (label && btn.contains(label)) label.textContent = 'Publishing…';
live.textContent = 'Working…';
setTimeout(() => {
btn.dataset.state = 'done';
btn.setAttribute('aria-busy', 'false');
if (label && btn.contains(label)) label.textContent = 'Published';
live.textContent = 'Done — saved just now';
setTimeout(() => {
btn.dataset.state = 'idle';
btn.disabled = false;
if (label && btn.contains(label)) label.textContent = 'Publish';
live.textContent = 'Ready';
}, 1800);
}, 1500);
});
});
})();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: Floating Button with Loading Spinner State CSS
Source: https://codefronts.com/components/css-floating-buttons/fb11/
The three-state async button — idle, pending, done — with the transitions between them treated as seriously as the states themselves. The spinner uses an animated dash gap so it never appears to stall, the success check draws in with stroke-dashoffset, and the whole thing is announced through aria-busy and a status region rather than by colour alone.
## HTML
```html
<section class="fb-29" aria-label="Floating button with loading state demo">
<div class="fb-29__stage">
<p class="fb-29__eyebrow">idle → loading → done → idle</p>
<h2 class="fb-29__title">Three states, one attribute</h2>
<p class="fb-29__sub">Click any button. The dash gap breathes so the spinner never looks frozen, the check draws in over 420ms, and it all resets after a beat.</p>
<div class="fb-29__row">
<button class="fb-29__fab" id="fb-29-a" type="button" data-state="idle" aria-label="Save document">
<span class="fb-29__ico" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path d="M5 4h11l3 3v13H5z" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linejoin="round"/><path d="M8 4v5h7M8 20v-6h8v6" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linejoin="round"/></svg></span>
<span class="fb-29__spin" aria-hidden="true"><svg viewBox="0 0 44 44" focusable="false"><circle cx="22" cy="22" r="17" fill="none" stroke="currentColor" stroke-width="3.4" stroke-linecap="round"/></svg></span>
<span class="fb-29__ok" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path class="fb-29__check" d="M5 12.6l4.6 4.6L19 7.4" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
</button>
<button class="fb-29__pill" id="fb-29-b" type="button" data-state="idle">
<span class="fb-29__iconstack" aria-hidden="true">
<span class="fb-29__ico"><svg viewBox="0 0 24 24" focusable="false"><path d="M12 17V6M8 10l4-4 4 4M5 19h14" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
<span class="fb-29__spin"><svg viewBox="0 0 44 44" focusable="false"><circle cx="22" cy="22" r="17" fill="none" stroke="currentColor" stroke-width="3.4" stroke-linecap="round"/></svg></span>
<span class="fb-29__ok"><svg viewBox="0 0 24 24" focusable="false"><path class="fb-29__check" d="M5 12.6l4.6 4.6L19 7.4" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
</span>
<b class="fb-29__label" id="fb-29-blabel">Publish</b>
</button>
</div>
<p class="fb-29__live" id="fb-29-live" role="status" aria-live="polite">Ready</p>
<p class="fb-29__chip">aria-busy · 400ms spinner floor · auto-reset 1.8s</p>
</div>
</section>
```
## CSS
```css
.fb-29 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--fb-29-bg);
--fb-29-bg: linear-gradient(180deg,oklch(0.98 0.008 265),oklch(0.94 0.02 258));
--fb-29-brand: oklch(0.52 0.17 265);
--fb-29-brandDeep: oklch(0.38 0.15 268);
--fb-29-ok: oklch(0.62 0.16 148);
--fb-29-ink: oklch(0.22 0.025 265);
--fb-29-mut: oklch(0.53 0.015 265);
font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
color: var(--fb-29-ink);
display: grid;
place-items: center;
padding: clamp(20px,5vw,56px);
}
.fb-29 *,
.fb-29 *::before,
.fb-29 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.fb-29__stage {
width: min(100%,56ch);
display: grid;
gap: clamp(16px,3vw,26px);
justify-items: center;
text-align: center;
}
.fb-29__eyebrow {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--fb-29-brand);
}
.fb-29__title {
font-size: clamp(26px,4.6vw,44px);
line-height: 1.05;
letter-spacing: -.03em;
font-weight: 700;
text-wrap: balance;
}
.fb-29__sub {
max-width: 50ch;
font-size: clamp(13.5px,1.7vw,16.5px);
line-height: 1.62;
color: var(--fb-29-mut);
text-wrap: pretty;
}
.fb-29__row {
display: flex;
flex-wrap: wrap;
gap: 22px;
justify-content: center;
align-items: center;
padding: 8px 0;
}
.fb-29__fab,
.fb-29__pill {
position: relative;
border: 0;
border-radius: 22px;
cursor: pointer;
color: oklch(0.99 0.01 265);
background: linear-gradient(150deg,oklch(0.6 0.18 262),var(--fb-29-brandDeep));
box-shadow: 0 14px 32px -12px oklch(0.38 0.15 268/.85),0 2px 6px oklch(0.3 0.07 268/.28);
transition: transform .26s cubic-bezier(.2,1.3,.4,1),background .3s ease,box-shadow .3s ease;
}
.fb-29__fab {
display: grid;
place-items: center;
inline-size: 64px;
block-size: 64px;
}
.fb-29__pill {
display: inline-flex;
align-items: center;
gap: 11px;
block-size: 64px;
padding-inline: 22px 26px;
border-radius: 99px;
font-family: inherit;
font-size: 16px;
font-weight: 640;
}
.fb-29__pill .fb-29__iconstack {
position: relative;
display: grid;
place-items: center;
inline-size: 30px;
block-size: 30px;
flex: none;
}
.fb-29__fab:hover,
.fb-29__pill:hover {
transform: translateY(-2px);
}
.fb-29__fab:active,
.fb-29__pill:active {
transform: scale(.96);
}
.fb-29__fab:focus-visible,
.fb-29__pill:focus-visible {
outline: 3px solid var(--fb-29-brandDeep);
outline-offset: 4px;
}
.fb-29__fab>.fb-29__ico,
.fb-29__fab>.fb-29__spin,
.fb-29__fab>.fb-29__ok,
.fb-29__pill .fb-29__iconstack>.fb-29__ico,
.fb-29__pill .fb-29__iconstack>.fb-29__spin,
.fb-29__pill .fb-29__iconstack>.fb-29__ok {
grid-area: 1/1;
display: grid;
place-items: center;
transition: opacity .2s ease,scale .28s cubic-bezier(.2,1.4,.4,1);
}
.fb-29__ico svg,
.fb-29__ok svg {
width: 26px;
height: 26px;
}
.fb-29__spin svg {
width: 30px;
height: 30px;
animation: fb-29-rot 1.1s linear infinite;
}
.fb-29__spin circle {
stroke-dasharray: 22 90;
animation: fb-29-dash 1.5s ease-in-out infinite;
}
@keyframes fb-29-rot {
to {
rotate: 360deg;
}
}
@keyframes fb-29-dash {
0% {
stroke-dasharray: 12 96;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 62 46;
stroke-dashoffset: -16;
}
100% {
stroke-dasharray: 12 96;
stroke-dashoffset: -104;
}
}
.fb-29__spin,
.fb-29__ok {
opacity: 0;
scale: .6;
pointer-events: none;
}
.fb-29__check {
stroke-dasharray: var(--fb-29-len,32);
stroke-dashoffset: var(--fb-29-len,32);
}
[data-state="loading"] .fb-29__ico {
opacity: 0;
scale: .6;
}
[data-state="loading"] .fb-29__spin {
opacity: 1;
scale: 1;
}
[data-state="done"] .fb-29__ico {
opacity: 0;
scale: .6;
}
[data-state="done"] .fb-29__ok {
opacity: 1;
scale: 1;
}
[data-state="done"] .fb-29__check {
animation: fb-29-draw .42s cubic-bezier(.2,.9,.3,1) forwards;
}
@keyframes fb-29-draw {
to {
stroke-dashoffset: 0;
}
}
.fb-29__fab[data-state="done"],
.fb-29__pill[data-state="done"] {
background: linear-gradient(150deg,oklch(0.7 0.16 150),oklch(0.5 0.14 150));
box-shadow: 0 14px 32px -12px oklch(0.5 0.14 150/.8);
}
.fb-29__fab[data-state="loading"],
.fb-29__pill[data-state="loading"] {
cursor: progress;
}
.fb-29__label {
font-weight: 640;
letter-spacing: -.01em;
}
.fb-29__live {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 12px;
color: var(--fb-29-brandDeep);
background: oklch(0.52 0.17 265/.09);
border-radius: 10px;
padding: 8px 15px;
min-width: 12ch;
}
.fb-29__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11px;
color: var(--fb-29-mut);
padding: 8px 14px;
border: 1px solid oklch(0.22 0.025 265/.14);
border-radius: 99px;
}
@media (prefers-reduced-motion: reduce) {
.fb-29 * {
animation-duration: .01ms !important;
transition: none !important;
}
.fb-29__check {
stroke-dashoffset: 0;
}
}
```
## JavaScript
```js
(() => {
const live = document.getElementById('fb-29-live');
const label = document.getElementById('fb-29-blabel');
const buttons = [document.getElementById('fb-29-a'), document.getElementById('fb-29-b')].filter(Boolean);
if (!buttons.length || buttons[0].dataset.fbWired) return;
buttons[0].dataset.fbWired = '1';
document.querySelectorAll('.fb-29__check').forEach((p) => {
const len = Math.ceil(p.getTotalLength() || 32);
p.style.setProperty('--fb-29-len', len);
});
buttons.forEach((btn) => {
btn.addEventListener('click', () => {
if (btn.dataset.state !== 'idle') return;
btn.dataset.state = 'loading';
btn.setAttribute('aria-busy', 'true');
btn.disabled = true;
if (label && btn.contains(label)) label.textContent = 'Publishing…';
live.textContent = 'Working…';
setTimeout(() => {
btn.dataset.state = 'done';
btn.setAttribute('aria-busy', 'false');
if (label && btn.contains(label)) label.textContent = 'Published';
live.textContent = 'Done — saved just now';
setTimeout(() => {
btn.dataset.state = 'idle';
btn.disabled = false;
if (label && btn.contains(label)) label.textContent = 'Publish';
live.textContent = 'Ready';
}, 1800);
}, 1500);
});
});
})();
```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: Floating Button with Loading Spinner State CSS
Source: https://codefronts.com/components/css-floating-buttons/fb11/
The three-state async button — idle, pending, done — with the transitions between them treated as seriously as the states themselves. The spinner uses an animated dash gap so it never appears to stall, the success check draws in with stroke-dashoffset, and the whole thing is announced through aria-busy and a status region rather than by colour alone.
## HTML
```html
<section class="fb-29" aria-label="Floating button with loading state demo">
<div class="fb-29__stage">
<p class="fb-29__eyebrow">idle → loading → done → idle</p>
<h2 class="fb-29__title">Three states, one attribute</h2>
<p class="fb-29__sub">Click any button. The dash gap breathes so the spinner never looks frozen, the check draws in over 420ms, and it all resets after a beat.</p>
<div class="fb-29__row">
<button class="fb-29__fab" id="fb-29-a" type="button" data-state="idle" aria-label="Save document">
<span class="fb-29__ico" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path d="M5 4h11l3 3v13H5z" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linejoin="round"/><path d="M8 4v5h7M8 20v-6h8v6" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linejoin="round"/></svg></span>
<span class="fb-29__spin" aria-hidden="true"><svg viewBox="0 0 44 44" focusable="false"><circle cx="22" cy="22" r="17" fill="none" stroke="currentColor" stroke-width="3.4" stroke-linecap="round"/></svg></span>
<span class="fb-29__ok" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path class="fb-29__check" d="M5 12.6l4.6 4.6L19 7.4" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
</button>
<button class="fb-29__pill" id="fb-29-b" type="button" data-state="idle">
<span class="fb-29__iconstack" aria-hidden="true">
<span class="fb-29__ico"><svg viewBox="0 0 24 24" focusable="false"><path d="M12 17V6M8 10l4-4 4 4M5 19h14" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
<span class="fb-29__spin"><svg viewBox="0 0 44 44" focusable="false"><circle cx="22" cy="22" r="17" fill="none" stroke="currentColor" stroke-width="3.4" stroke-linecap="round"/></svg></span>
<span class="fb-29__ok"><svg viewBox="0 0 24 24" focusable="false"><path class="fb-29__check" d="M5 12.6l4.6 4.6L19 7.4" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
</span>
<b class="fb-29__label" id="fb-29-blabel">Publish</b>
</button>
</div>
<p class="fb-29__live" id="fb-29-live" role="status" aria-live="polite">Ready</p>
<p class="fb-29__chip">aria-busy · 400ms spinner floor · auto-reset 1.8s</p>
</div>
</section>
```
## CSS
```css
.fb-29 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--fb-29-bg);
--fb-29-bg: linear-gradient(180deg,oklch(0.98 0.008 265),oklch(0.94 0.02 258));
--fb-29-brand: oklch(0.52 0.17 265);
--fb-29-brandDeep: oklch(0.38 0.15 268);
--fb-29-ok: oklch(0.62 0.16 148);
--fb-29-ink: oklch(0.22 0.025 265);
--fb-29-mut: oklch(0.53 0.015 265);
font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
color: var(--fb-29-ink);
display: grid;
place-items: center;
padding: clamp(20px,5vw,56px);
}
.fb-29 *,
.fb-29 *::before,
.fb-29 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.fb-29__stage {
width: min(100%,56ch);
display: grid;
gap: clamp(16px,3vw,26px);
justify-items: center;
text-align: center;
}
.fb-29__eyebrow {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--fb-29-brand);
}
.fb-29__title {
font-size: clamp(26px,4.6vw,44px);
line-height: 1.05;
letter-spacing: -.03em;
font-weight: 700;
text-wrap: balance;
}
.fb-29__sub {
max-width: 50ch;
font-size: clamp(13.5px,1.7vw,16.5px);
line-height: 1.62;
color: var(--fb-29-mut);
text-wrap: pretty;
}
.fb-29__row {
display: flex;
flex-wrap: wrap;
gap: 22px;
justify-content: center;
align-items: center;
padding: 8px 0;
}
.fb-29__fab,
.fb-29__pill {
position: relative;
border: 0;
border-radius: 22px;
cursor: pointer;
color: oklch(0.99 0.01 265);
background: linear-gradient(150deg,oklch(0.6 0.18 262),var(--fb-29-brandDeep));
box-shadow: 0 14px 32px -12px oklch(0.38 0.15 268/.85),0 2px 6px oklch(0.3 0.07 268/.28);
transition: transform .26s cubic-bezier(.2,1.3,.4,1),background .3s ease,box-shadow .3s ease;
}
.fb-29__fab {
display: grid;
place-items: center;
inline-size: 64px;
block-size: 64px;
}
.fb-29__pill {
display: inline-flex;
align-items: center;
gap: 11px;
block-size: 64px;
padding-inline: 22px 26px;
border-radius: 99px;
font-family: inherit;
font-size: 16px;
font-weight: 640;
}
.fb-29__pill .fb-29__iconstack {
position: relative;
display: grid;
place-items: center;
inline-size: 30px;
block-size: 30px;
flex: none;
}
.fb-29__fab:hover,
.fb-29__pill:hover {
transform: translateY(-2px);
}
.fb-29__fab:active,
.fb-29__pill:active {
transform: scale(.96);
}
.fb-29__fab:focus-visible,
.fb-29__pill:focus-visible {
outline: 3px solid var(--fb-29-brandDeep);
outline-offset: 4px;
}
.fb-29__fab>.fb-29__ico,
.fb-29__fab>.fb-29__spin,
.fb-29__fab>.fb-29__ok,
.fb-29__pill .fb-29__iconstack>.fb-29__ico,
.fb-29__pill .fb-29__iconstack>.fb-29__spin,
.fb-29__pill .fb-29__iconstack>.fb-29__ok {
grid-area: 1/1;
display: grid;
place-items: center;
transition: opacity .2s ease,scale .28s cubic-bezier(.2,1.4,.4,1);
}
.fb-29__ico svg,
.fb-29__ok svg {
width: 26px;
height: 26px;
}
.fb-29__spin svg {
width: 30px;
height: 30px;
animation: fb-29-rot 1.1s linear infinite;
}
.fb-29__spin circle {
stroke-dasharray: 22 90;
animation: fb-29-dash 1.5s ease-in-out infinite;
}
@keyframes fb-29-rot {
to {
rotate: 360deg;
}
}
@keyframes fb-29-dash {
0% {
stroke-dasharray: 12 96;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 62 46;
stroke-dashoffset: -16;
}
100% {
stroke-dasharray: 12 96;
stroke-dashoffset: -104;
}
}
.fb-29__spin,
.fb-29__ok {
opacity: 0;
scale: .6;
pointer-events: none;
}
.fb-29__check {
stroke-dasharray: var(--fb-29-len,32);
stroke-dashoffset: var(--fb-29-len,32);
}
[data-state="loading"] .fb-29__ico {
opacity: 0;
scale: .6;
}
[data-state="loading"] .fb-29__spin {
opacity: 1;
scale: 1;
}
[data-state="done"] .fb-29__ico {
opacity: 0;
scale: .6;
}
[data-state="done"] .fb-29__ok {
opacity: 1;
scale: 1;
}
[data-state="done"] .fb-29__check {
animation: fb-29-draw .42s cubic-bezier(.2,.9,.3,1) forwards;
}
@keyframes fb-29-draw {
to {
stroke-dashoffset: 0;
}
}
.fb-29__fab[data-state="done"],
.fb-29__pill[data-state="done"] {
background: linear-gradient(150deg,oklch(0.7 0.16 150),oklch(0.5 0.14 150));
box-shadow: 0 14px 32px -12px oklch(0.5 0.14 150/.8);
}
.fb-29__fab[data-state="loading"],
.fb-29__pill[data-state="loading"] {
cursor: progress;
}
.fb-29__label {
font-weight: 640;
letter-spacing: -.01em;
}
.fb-29__live {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 12px;
color: var(--fb-29-brandDeep);
background: oklch(0.52 0.17 265/.09);
border-radius: 10px;
padding: 8px 15px;
min-width: 12ch;
}
.fb-29__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11px;
color: var(--fb-29-mut);
padding: 8px 14px;
border: 1px solid oklch(0.22 0.025 265/.14);
border-radius: 99px;
}
@media (prefers-reduced-motion: reduce) {
.fb-29 * {
animation-duration: .01ms !important;
transition: none !important;
}
.fb-29__check {
stroke-dashoffset: 0;
}
}
```
## JavaScript
```js
(() => {
const live = document.getElementById('fb-29-live');
const label = document.getElementById('fb-29-blabel');
const buttons = [document.getElementById('fb-29-a'), document.getElementById('fb-29-b')].filter(Boolean);
if (!buttons.length || buttons[0].dataset.fbWired) return;
buttons[0].dataset.fbWired = '1';
document.querySelectorAll('.fb-29__check').forEach((p) => {
const len = Math.ceil(p.getTotalLength() || 32);
p.style.setProperty('--fb-29-len', len);
});
buttons.forEach((btn) => {
btn.addEventListener('click', () => {
if (btn.dataset.state !== 'idle') return;
btn.dataset.state = 'loading';
btn.setAttribute('aria-busy', 'true');
btn.disabled = true;
if (label && btn.contains(label)) label.textContent = 'Publishing…';
live.textContent = 'Working…';
setTimeout(() => {
btn.dataset.state = 'done';
btn.setAttribute('aria-busy', 'false');
if (label && btn.contains(label)) label.textContent = 'Published';
live.textContent = 'Done — saved just now';
setTimeout(() => {
btn.dataset.state = 'idle';
btn.disabled = false;
if (label && btn.contains(label)) label.textContent = 'Publish';
live.textContent = 'Ready';
}, 1800);
}, 1500);
});
});
})();
```How this works
A single data-state attribute drives the CSS; the JS only ever changes that one value:
[data-state="loading"] .icon{ opacity:0; scale:.6 }
[data-state="loading"] .spinner{ opacity:1 }
[data-state="done"] .check{ animation:draw .42s cubic-bezier(.2,.9,.3,1) forwards }
@keyframes draw{ to{ stroke-dashoffset:0 } }The spinner is an SVG circle with stroke-dasharray animated between two lengths while the whole element rotates. A fixed-length arc spinning at constant speed reads as frozen when it happens to sync with your eye; a breathing dash gap never does.
The check mark is one path with its stroke-dasharray and stroke-dashoffset both set to the path length, animated to zero — the standard draw-on technique, and the most convincing "complete" cue there is, because it takes time the way a real confirmation does.
Accessibility: the button gets aria-busy="true" and disabled while pending (so a double-click cannot fire twice), a role="status" region announces "Saving" and then "Saved", and after 1.8s it returns to idle. Never leave a button permanently in a success state — the user's next action needs the idle affordance back.
Make it yours
- Progress instead of spin: if you know the percentage, animate stroke-dashoffset from the real value — a determinate ring always beats a spinner.
- Error state: a fourth data-state with a shake (three 4px translations) and an X draw, returning to idle after 2.5s.
- Extended pill: the same three states work on a labelled pill where the text swaps Save → Saving… → Saved.
- Optimistic UI: skip the pending state for actions under ~300ms and go straight to done — a spinner that flashes is worse than none.
Gotchas — read before shipping
- Disabling the button without aria-busy tells assistive tech the control is unavailable, not that it is working. Use both.
- A spinner shown for under 200ms flashes and reads as a glitch. Either delay showing it, or hold it for a 400ms minimum once shown.
- Do not change the button's size between states; a shrinking button moves the pointer target mid-interaction.
- Success states that persist indefinitely block the next action. Always time out back to idle.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 114+ | 17.5+ | 121+ | 114+ |
Floor set by oklch(), text-wrap as this demo is written. The core technique itself goes back further — Chrome 111+.
SVG stroke animation and keyframes work in every browser. getTotalLength() is used to measure the check path and is supported since IE9.