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.

CSS + JS MIT licensed
Live Demo Open in tab
Open in playground

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>
.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' }));

Search CodeFronts

Loading…