32 CSS Floating Action Button Designs 04 / 32
Scroll-to-Top Progress Ring
Scroll-to-top floating button with SVG circular progress ring that tracks scroll depth and fades in via IntersectionObserver.
The code
<div class="fb04">
<!-- Scroll to top buttons (fixed) -->
<button id="fb04-stt-btn" aria-label="Back to top">
<!-- progress ring -->
<svg class="fb04-ring" viewBox="0 0 68 68" aria-hidden="true">
<circle class="fb04-ring-track" cx="34" cy="34" r="30"/>
<circle class="fb04-ring-progress" id="fb04-ring-prog" cx="34" cy="34" r="30"/>
</svg>
<svg class="fb04-arrow-up" viewBox="0 0 24 24" aria-hidden="true">
<path d="M4 12l1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z"/>
</svg>
<span class="fb04-pct-label" id="fb04-stt-pct">0%</span>
</button>
<button class="fb04-mini-stt" id="fb04-mini-stt" aria-label="Back to top">
<svg viewBox="0 0 24 24"><path d="M4 12l1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z"/></svg>
</button>
<!-- Page content -->
<section class="fb04-hero" id="fb04-top">
<h1>Scroll down to<br>see the FAB appear</h1>
<p>A scroll-to-top floating button with an SVG progress ring that tracks your reading position — pure CSS styling, tiny vanilla JS for the scroll math.</p>
<div class="fb04-scroll-hint">Scroll</div>
</section>
<div class="fb04-content-section">
<h2>The anatomy of a great scroll-to-top button</h2>
<p>A scroll-to-top FAB earns its screen space by being invisible until the user has scrolled far enough that returning to the top would be a real chore — typically after 20–30% of page height.</p>
<p>The progress ring wraps the button and fills as the user scrolls, giving a peripheral readout of reading progress without cluttering the layout with a separate indicator bar.</p>
<div class="fb04-divider"></div>
<h2>Design principles at work</h2>
<p>The button enters with a spring scale-in so it feels physical, not merely functional. On hover it lifts slightly and reveals a percentage tooltip confirming the ring reading. On click, smooth scrolling returns the user to the top.</p>
<p>Keep the button at least 60px from the edge on mobile to clear thumb-friendly zones. The mini variant in the bottom-left shows a minimal borderless square treatment — useful when the bottom-right is occupied by a chat widget or another FAB.</p>
<div class="fb04-divider"></div>
<h2>Implementation notes</h2>
<p>The SVG ring uses <code>stroke-dasharray</code> and <code>stroke-dashoffset</code> to trace progress. The circumference of the circle (2πr = 2π×30 ≈ 188.5) defines <code>dasharray</code>; the JS maps scroll percentage to dashoffset between 188.5 and 0.</p>
<p>Visibility is toggled by adding a <code>.visible</code> class via an IntersectionObserver on the hero section, avoiding scroll-event overhead for the show/hide trigger.</p>
<div class="fb04-divider"></div>
<h2>Accessibility</h2>
<p>The button carries an <code>aria-label="Back to top"</code> and an <code>id="fb04-top"</code> anchor on the hero. It is focusable, keyboard-activatable, and hides from the tab order when not visible via <code>pointer-events: none</code> combined with CSS opacity.</p>
<p>The ring SVG is marked <code>aria-hidden</code> so screen readers skip the decorative progress graphic. A <code>prefers-reduced-motion</code> query degrades the entrance animation to a simple fade.</p>
<div class="fb04-divider"></div>
<h2>The compact square variant</h2>
<p>On the left side you'll see a minimal 44×44px borderless square FAB — same function, half the footprint. It suits content-dense dashboards where a 60px circular FAB with ring would feel heavy. No progress ring, just the arrow — simplicity as a deliberate choice.</p>
</div>
</div> <div class="fb04">
<!-- Scroll to top buttons (fixed) -->
<button id="fb04-stt-btn" aria-label="Back to top">
<!-- progress ring -->
<svg class="fb04-ring" viewBox="0 0 68 68" aria-hidden="true">
<circle class="fb04-ring-track" cx="34" cy="34" r="30"/>
<circle class="fb04-ring-progress" id="fb04-ring-prog" cx="34" cy="34" r="30"/>
</svg>
<svg class="fb04-arrow-up" viewBox="0 0 24 24" aria-hidden="true">
<path d="M4 12l1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z"/>
</svg>
<span class="fb04-pct-label" id="fb04-stt-pct">0%</span>
</button>
<button class="fb04-mini-stt" id="fb04-mini-stt" aria-label="Back to top">
<svg viewBox="0 0 24 24"><path d="M4 12l1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z"/></svg>
</button>
<!-- Page content -->
<section class="fb04-hero" id="fb04-top">
<h1>Scroll down to<br>see the FAB appear</h1>
<p>A scroll-to-top floating button with an SVG progress ring that tracks your reading position — pure CSS styling, tiny vanilla JS for the scroll math.</p>
<div class="fb04-scroll-hint">Scroll</div>
</section>
<div class="fb04-content-section">
<h2>The anatomy of a great scroll-to-top button</h2>
<p>A scroll-to-top FAB earns its screen space by being invisible until the user has scrolled far enough that returning to the top would be a real chore — typically after 20–30% of page height.</p>
<p>The progress ring wraps the button and fills as the user scrolls, giving a peripheral readout of reading progress without cluttering the layout with a separate indicator bar.</p>
<div class="fb04-divider"></div>
<h2>Design principles at work</h2>
<p>The button enters with a spring scale-in so it feels physical, not merely functional. On hover it lifts slightly and reveals a percentage tooltip confirming the ring reading. On click, smooth scrolling returns the user to the top.</p>
<p>Keep the button at least 60px from the edge on mobile to clear thumb-friendly zones. The mini variant in the bottom-left shows a minimal borderless square treatment — useful when the bottom-right is occupied by a chat widget or another FAB.</p>
<div class="fb04-divider"></div>
<h2>Implementation notes</h2>
<p>The SVG ring uses <code>stroke-dasharray</code> and <code>stroke-dashoffset</code> to trace progress. The circumference of the circle (2πr = 2π×30 ≈ 188.5) defines <code>dasharray</code>; the JS maps scroll percentage to dashoffset between 188.5 and 0.</p>
<p>Visibility is toggled by adding a <code>.visible</code> class via an IntersectionObserver on the hero section, avoiding scroll-event overhead for the show/hide trigger.</p>
<div class="fb04-divider"></div>
<h2>Accessibility</h2>
<p>The button carries an <code>aria-label="Back to top"</code> and an <code>id="fb04-top"</code> anchor on the hero. It is focusable, keyboard-activatable, and hides from the tab order when not visible via <code>pointer-events: none</code> combined with CSS opacity.</p>
<p>The ring SVG is marked <code>aria-hidden</code> so screen readers skip the decorative progress graphic. A <code>prefers-reduced-motion</code> query degrades the entrance animation to a simple fade.</p>
<div class="fb04-divider"></div>
<h2>The compact square variant</h2>
<p>On the left side you'll see a minimal 44×44px borderless square FAB — same function, half the footprint. It suits content-dense dashboards where a 60px circular FAB with ring would feel heavy. No progress ring, just the arrow — simplicity as a deliberate choice.</p>
</div>
</div>.fb04, .fb04 *, .fb04 *::before, .fb04 *::after { box-sizing: border-box; margin: 0; padding: 0; }
.fb04 { scroll-behavior: smooth; }
.fb04 {
font-family: 'Inter', sans-serif;
background: #f8f8f5;
color: #2d2d2d;
line-height: 1.7;
}
/* ── Long scrollable page content ── */
.fb04-hero {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 60px 24px;
background: linear-gradient(160deg, #1e1b4b 0%, #312e81 50%, #4f46e5 100%);
color: #fff;
}
.fb04-hero h1 {
font-family: 'Instrument Serif', serif;
font-size: clamp(2.4rem, 8vw, 5rem);
line-height: 1.1;
margin-bottom: 20px;
font-style: italic;
}
.fb04-hero p {
font-size: clamp(1rem, 2.5vw, 1.2rem);
color: rgba(255,255,255,.7);
max-width: 46ch;
}
.fb04-scroll-hint {
margin-top: 48px;
font-size: .82rem;
letter-spacing: .1em;
text-transform: uppercase;
color: rgba(255,255,255,.45);
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
animation: fb04-hint-bob 2s ease-in-out infinite;
}
.fb04-scroll-hint::after {
content: '';
width: 1.5px;
height: 40px;
background: rgba(255,255,255,.3);
border-radius: 1px;
}
@keyframes fb04-hint-bob { 50% { transform: translateY(6px); } }
.fb04-content-section {
max-width: 720px;
margin: 0 auto;
padding: 80px 24px;
}
.fb04-content-section h2 {
font-family: 'Instrument Serif', serif;
font-size: clamp(1.8rem, 4vw, 2.6rem);
margin-bottom: 20px;
color: #1e1b4b;
font-style: italic;
}
.fb04-content-section p { color: #555; margin-bottom: 18px; }
.fb04-divider { height: 1px; background: #e5e5e0; margin: 60px 0; }
/* ── SCROLL TO TOP FAB ── */
#fb04-stt-btn {
position: fixed;
bottom: 32px;
right: 32px;
width: 60px;
height: 60px;
border-radius: 50%;
border: none;
cursor: pointer;
background: #1e1b4b;
color: #fff;
display: grid;
place-items: center;
box-shadow: 0 6px 24px rgba(30,27,75,.4);
opacity: 0;
transform: translateY(20px) scale(.85);
transition: opacity .3s ease, transform .3s cubic-bezier(.34,1.56,.64,1), box-shadow .2s ease;
z-index: 1000;
/* prevent interaction when hidden */
pointer-events: none;
}
#fb04-stt-btn.fb04-visible {
opacity: 1;
transform: none;
pointer-events: auto;
}
#fb04-stt-btn:hover {
box-shadow: 0 10px 32px rgba(30,27,75,.55);
background: #312e81;
}
#fb04-stt-btn:active { transform: scale(.95); }
/* SVG progress ring */
#fb04-stt-btn svg.fb04-ring {
position: absolute;
inset: -4px;
width: calc(100% + 8px);
height: calc(100% + 8px);
transform: rotate(-90deg);
pointer-events: none;
}
#fb04-stt-btn .fb04-ring-track {
fill: none;
stroke: rgba(255,255,255,.12);
stroke-width: 3;
}
#fb04-stt-btn .fb04-ring-progress {
fill: none;
stroke: #818cf8;
stroke-width: 3;
stroke-linecap: round;
stroke-dasharray: 200;
stroke-dashoffset: 200;
transition: stroke-dashoffset .1s linear;
}
/* arrow icon */
#fb04-stt-btn .fb04-arrow-up {
width: 22px;
height: 22px;
fill: #fff;
position: relative;
z-index: 1;
transition: transform .2s ease;
}
#fb04-stt-btn:hover .fb04-arrow-up { transform: translateY(-2px); }
/* percentage label (shows on hover) */
#fb04-stt-btn .fb04-pct-label {
position: absolute;
top: -38px;
left: 50%;
transform: translateX(-50%);
background: #1e1b4b;
color: #a5b4fc;
font-size: .7rem;
font-weight: 600;
letter-spacing: .06em;
padding: 4px 8px;
border-radius: 6px;
white-space: nowrap;
opacity: 0;
transition: opacity .2s;
pointer-events: none;
}
#fb04-stt-btn:hover .fb04-pct-label { opacity: 1; }
/* ── alternate compact variant shown in corner ── */
.fb04-mini-stt {
position: fixed;
bottom: 32px;
left: 32px;
width: 44px;
height: 44px;
border-radius: 12px;
border: 1.5px solid #e5e7eb;
background: #fff;
cursor: pointer;
display: grid;
place-items: center;
box-shadow: 0 2px 12px rgba(0,0,0,.1);
opacity: 0;
transform: translateY(16px);
transition: opacity .3s ease, transform .3s ease, box-shadow .2s ease;
pointer-events: none;
z-index: 999;
}
.fb04-mini-stt.fb04-visible { opacity: 1; transform: none; pointer-events: auto; }
.fb04-mini-stt:hover { box-shadow: 0 6px 20px rgba(0,0,0,.14); }
.fb04-mini-stt svg { width: 18px; height: 18px; fill: #6366f1; } .fb04, .fb04 *, .fb04 *::before, .fb04 *::after { box-sizing: border-box; margin: 0; padding: 0; }
.fb04 { scroll-behavior: smooth; }
.fb04 {
font-family: 'Inter', sans-serif;
background: #f8f8f5;
color: #2d2d2d;
line-height: 1.7;
}
/* ── Long scrollable page content ── */
.fb04-hero {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 60px 24px;
background: linear-gradient(160deg, #1e1b4b 0%, #312e81 50%, #4f46e5 100%);
color: #fff;
}
.fb04-hero h1 {
font-family: 'Instrument Serif', serif;
font-size: clamp(2.4rem, 8vw, 5rem);
line-height: 1.1;
margin-bottom: 20px;
font-style: italic;
}
.fb04-hero p {
font-size: clamp(1rem, 2.5vw, 1.2rem);
color: rgba(255,255,255,.7);
max-width: 46ch;
}
.fb04-scroll-hint {
margin-top: 48px;
font-size: .82rem;
letter-spacing: .1em;
text-transform: uppercase;
color: rgba(255,255,255,.45);
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
animation: fb04-hint-bob 2s ease-in-out infinite;
}
.fb04-scroll-hint::after {
content: '';
width: 1.5px;
height: 40px;
background: rgba(255,255,255,.3);
border-radius: 1px;
}
@keyframes fb04-hint-bob { 50% { transform: translateY(6px); } }
.fb04-content-section {
max-width: 720px;
margin: 0 auto;
padding: 80px 24px;
}
.fb04-content-section h2 {
font-family: 'Instrument Serif', serif;
font-size: clamp(1.8rem, 4vw, 2.6rem);
margin-bottom: 20px;
color: #1e1b4b;
font-style: italic;
}
.fb04-content-section p { color: #555; margin-bottom: 18px; }
.fb04-divider { height: 1px; background: #e5e5e0; margin: 60px 0; }
/* ── SCROLL TO TOP FAB ── */
#fb04-stt-btn {
position: fixed;
bottom: 32px;
right: 32px;
width: 60px;
height: 60px;
border-radius: 50%;
border: none;
cursor: pointer;
background: #1e1b4b;
color: #fff;
display: grid;
place-items: center;
box-shadow: 0 6px 24px rgba(30,27,75,.4);
opacity: 0;
transform: translateY(20px) scale(.85);
transition: opacity .3s ease, transform .3s cubic-bezier(.34,1.56,.64,1), box-shadow .2s ease;
z-index: 1000;
/* prevent interaction when hidden */
pointer-events: none;
}
#fb04-stt-btn.fb04-visible {
opacity: 1;
transform: none;
pointer-events: auto;
}
#fb04-stt-btn:hover {
box-shadow: 0 10px 32px rgba(30,27,75,.55);
background: #312e81;
}
#fb04-stt-btn:active { transform: scale(.95); }
/* SVG progress ring */
#fb04-stt-btn svg.fb04-ring {
position: absolute;
inset: -4px;
width: calc(100% + 8px);
height: calc(100% + 8px);
transform: rotate(-90deg);
pointer-events: none;
}
#fb04-stt-btn .fb04-ring-track {
fill: none;
stroke: rgba(255,255,255,.12);
stroke-width: 3;
}
#fb04-stt-btn .fb04-ring-progress {
fill: none;
stroke: #818cf8;
stroke-width: 3;
stroke-linecap: round;
stroke-dasharray: 200;
stroke-dashoffset: 200;
transition: stroke-dashoffset .1s linear;
}
/* arrow icon */
#fb04-stt-btn .fb04-arrow-up {
width: 22px;
height: 22px;
fill: #fff;
position: relative;
z-index: 1;
transition: transform .2s ease;
}
#fb04-stt-btn:hover .fb04-arrow-up { transform: translateY(-2px); }
/* percentage label (shows on hover) */
#fb04-stt-btn .fb04-pct-label {
position: absolute;
top: -38px;
left: 50%;
transform: translateX(-50%);
background: #1e1b4b;
color: #a5b4fc;
font-size: .7rem;
font-weight: 600;
letter-spacing: .06em;
padding: 4px 8px;
border-radius: 6px;
white-space: nowrap;
opacity: 0;
transition: opacity .2s;
pointer-events: none;
}
#fb04-stt-btn:hover .fb04-pct-label { opacity: 1; }
/* ── alternate compact variant shown in corner ── */
.fb04-mini-stt {
position: fixed;
bottom: 32px;
left: 32px;
width: 44px;
height: 44px;
border-radius: 12px;
border: 1.5px solid #e5e7eb;
background: #fff;
cursor: pointer;
display: grid;
place-items: center;
box-shadow: 0 2px 12px rgba(0,0,0,.1);
opacity: 0;
transform: translateY(16px);
transition: opacity .3s ease, transform .3s ease, box-shadow .2s ease;
pointer-events: none;
z-index: 999;
}
.fb04-mini-stt.fb04-visible { opacity: 1; transform: none; pointer-events: auto; }
.fb04-mini-stt:hover { box-shadow: 0 6px 20px rgba(0,0,0,.14); }
.fb04-mini-stt svg { width: 18px; height: 18px; fill: #6366f1; }const btn = document.getElementById('fb04-stt-btn');
const mini = document.getElementById('fb04-mini-stt');
const prog = document.getElementById('fb04-ring-prog');
const pctLabel = document.getElementById('fb04-stt-pct');
const CIRCUMFERENCE = 2 * Math.PI * 30; // ≈ 188.5
function update() {
const scrollTop = window.scrollY;
const maxScroll = document.documentElement.scrollHeight - window.innerHeight;
const pct = maxScroll > 0 ? scrollTop / maxScroll : 0;
const offset = CIRCUMFERENCE * (1 - pct);
prog.style.strokeDasharray = CIRCUMFERENCE;
prog.style.strokeDashoffset = offset;
pctLabel.textContent = Math.round(pct * 100) + '%';
const show = scrollTop > window.innerHeight * 0.3;
btn.classList.toggle('fb04-visible', show);
mini.classList.toggle('fb04-visible', show);
}
window.addEventListener('scroll', update, { passive: true });
update();
btn.addEventListener('click', () => window.scrollTo({ top: 0, behavior: 'smooth' }));
mini.addEventListener('click', () => window.scrollTo({ top: 0, behavior: 'smooth' })); const btn = document.getElementById('fb04-stt-btn');
const mini = document.getElementById('fb04-mini-stt');
const prog = document.getElementById('fb04-ring-prog');
const pctLabel = document.getElementById('fb04-stt-pct');
const CIRCUMFERENCE = 2 * Math.PI * 30; // ≈ 188.5
function update() {
const scrollTop = window.scrollY;
const maxScroll = document.documentElement.scrollHeight - window.innerHeight;
const pct = maxScroll > 0 ? scrollTop / maxScroll : 0;
const offset = CIRCUMFERENCE * (1 - pct);
prog.style.strokeDasharray = CIRCUMFERENCE;
prog.style.strokeDashoffset = offset;
pctLabel.textContent = Math.round(pct * 100) + '%';
const show = scrollTop > window.innerHeight * 0.3;
btn.classList.toggle('fb04-visible', show);
mini.classList.toggle('fb04-visible', show);
}
window.addEventListener('scroll', update, { passive: true });
update();
btn.addEventListener('click', () => window.scrollTo({ top: 0, behavior: 'smooth' }));
mini.addEventListener('click', () => window.scrollTo({ top: 0, behavior: 'smooth' }));More from 32 CSS Floating Action Button Designs
Labeled PillScroll to TopNeon CyberGlass FABBrutalist StampNotification BadgeDrag HandlePremium AuroraQuick ReplySquare ModernFloating Chat WidgetFan Arc FAB Menu
View the full collection →