Back to CSS Scroll Animations Triple Scroll Progress CSS + JS
Share
HTML
<div class="bar" id="bar"></div>

<nav class="rail" id="rail">
  <button class="dot" data-target="c1"><span class="ring"></span><span class="lbl">I — Attention</span></button>
  <button class="dot" data-target="c2"><span class="ring"></span><span class="lbl">II — Friction</span></button>
  <button class="dot" data-target="c3"><span class="ring"></span><span class="lbl">III — Depth</span></button>
  <button class="dot" data-target="c4"><span class="ring"></span><span class="lbl">IV — Rhythm</span></button>
  <button class="dot" data-target="c5"><span class="ring"></span><span class="lbl">V — Closure</span></button>
</nav>

<div class="orb">
  <svg width="62" height="62" viewBox="0 0 62 62">
    <circle class="track" cx="31" cy="31" r="26"/>
    <circle class="prog" id="orbProg" cx="31" cy="31" r="26"/>
  </svg>
  <div class="pct" id="orbPct">0%</div>
</div>

<article>
  <div class="eyebrow">Essay · Reading Experience</div>
  <h1>The Quiet Architecture of Long-Form Attention</h1>
  <p class="standfirst">Three progress signals — a bar, a rail, an orb — each answering a different question the reader never asks aloud.</p>

  <section class="chapter" id="c1">
    <h2>I — Attention</h2>
    <p>Reading on a screen is an act of sustained negotiation. The text wants forward motion; the eye wants reassurance. A good interface offers both without demanding either, and the simplest gift it can give is a sense of place.</p>
    <p>The top bar is the most literal of these signals. It maps the scroll to a single horizontal line, and that line says only one thing: this much remains.</p>
    <div class="pull">A progress bar is a promise the page makes — small, honest, and easy to keep.</div>
  </section>

  <section class="chapter" id="c2">
    <h2>II — Friction</h2>
    <p>Friction is not the enemy of reading. The right kind of friction — a heading, a pause, a turn of phrase — slows the eye on purpose, and a chapter rail formalises those pauses into navigable anchors.</p>
    <p>Click a dot and the page travels. Scroll naturally and the active dot keeps pace. The reader is never told where they are; they simply notice.</p>
  </section>

  <section class="chapter" id="c3">
    <h2>III — Depth</h2>
    <p>Depth is the feeling that a piece has more underneath it than the surface admits. Typography carries most of that weight — the drop cap, the measured line length, the italic standfirst that sets a tone before the argument begins.</p>
    <p>None of it is decoration. Each choice is a quiet instruction about how fast to move and how much to trust.</p>
    <div class="pull">The page that respects its reader rarely raises its voice.</div>
  </section>

  <section class="chapter" id="c4">
    <h2>IV — Rhythm</h2>
    <p>Rhythm is the cadence of revelation. Paragraphs of even weight lull; paragraphs that vary — short, long, short — keep the eye awake. The orb in the corner tracks this rhythm as a percentage, a number that climbs while the reader forgets it exists.</p>
    <p>That forgetting is the point. The best progress indicator is the one you only consult when you wonder, not the one that nags.</p>
  </section>

  <section class="chapter" id="c5">
    <h2>V — Closure</h2>
    <p>Closure is the reward. The bar reaches full width, the final dot lights, the orb completes its circle — three small confirmations that the journey had a shape and the shape has ended.</p>
    <p>Long-form reading survives not because screens improved but because a few quiet signals learned to respect the time a reader gives.</p>
  </section>
</article>
CSS
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
body{background:#FBFAF7;font-family:'Inter',sans-serif;color:#1A1A1A}

/* top gradient progress bar */
.bar{
  position:fixed;top:0;left:0;height:3px;width:0%;
  background:linear-gradient(90deg,#6D28D9,#DB2777,#F59E0B);
  z-index:50;transition:width 0.1s linear;
}

/* left chapter rail */
.rail{
  position:fixed;left:2.5vw;top:50%;transform:translateY(-50%);
  display:flex;flex-direction:column;gap:1.1rem;z-index:40;
}
.dot{
  display:flex;align-items:center;gap:0.65rem;
  cursor:pointer;border:none;background:none;text-align:left;
}
.dot .ring{
  width:11px;height:11px;border-radius:50%;
  border:1.5px solid rgba(0,0,0,0.25);
  transition:background 0.3s,border-color 0.3s,transform 0.3s;
}
.dot.active .ring{background:#6D28D9;border-color:#6D28D9;transform:scale(1.35)}
.dot .lbl{
  font-size:11px;letter-spacing:0.04em;color:rgba(0,0,0,0.35);
  opacity:0;transform:translateX(-6px);
  transition:opacity 0.3s,transform 0.3s,color 0.3s;
}
.dot:hover .lbl,.dot.active .lbl{opacity:1;transform:translateX(0)}
.dot.active .lbl{color:#1A1A1A}

/* circular reading-time orb */
.orb{
  position:fixed;right:2.5vw;bottom:2.5vw;width:62px;height:62px;z-index:40;
}
.orb svg{transform:rotate(-90deg)}
.orb .track{fill:none;stroke:rgba(0,0,0,0.08);stroke-width:4}
.orb .prog{fill:none;stroke:#6D28D9;stroke-width:4;stroke-linecap:round;
  stroke-dasharray:163;stroke-dashoffset:163;transition:stroke-dashoffset 0.1s linear}
.orb .pct{
  position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
  font-size:12px;font-weight:600;font-family:'Fraunces',serif;
}

/* article */
article{max-width:660px;margin:0 auto;padding:9rem 1.5rem 12rem}
.eyebrow{
  font-size:11px;letter-spacing:0.18em;text-transform:uppercase;
  color:#DB2777;margin-bottom:1.25rem;
}
h1{
  font-family:'Fraunces',serif;font-weight:600;
  font-size:clamp(34px,5vw,56px);line-height:1.08;
  margin-bottom:1.5rem;letter-spacing:-0.02em;
}
.standfirst{
  font-size:19px;line-height:1.6;color:#555;
  margin-bottom:3.5rem;font-style:italic;
}
.chapter{padding-top:5rem;scroll-margin-top:5rem}
.chapter h2{
  font-family:'Fraunces',serif;font-weight:600;font-size:28px;
  margin-bottom:1.5rem;letter-spacing:-0.01em;
}
.chapter p{font-size:17px;line-height:1.78;color:#333;margin-bottom:1.4rem}

/* drop cap */
.chapter:first-of-type p:first-of-type::first-letter{
  font-family:'Fraunces',serif;font-weight:600;
  font-size:64px;float:left;line-height:0.82;
  margin:6px 12px 0 0;color:#6D28D9;
}

/* pull quote */
.pull{
  font-family:'Fraunces',serif;font-style:italic;
  font-size:25px;line-height:1.4;color:#1A1A1A;
  border-left:3px solid #DB2777;padding:0.4rem 0 0.4rem 1.6rem;
  margin:2.75rem 0;
}
JS
const bar=document.getElementById('bar');
const orbProg=document.getElementById('orbProg');
const orbPct=document.getElementById('orbPct');
const dots=[...document.querySelectorAll('.dot')];
const chapters=dots.map(d=>document.getElementById(d.dataset.target));
const CIRC=2*Math.PI*26; // 2πr

function update(){
  const doc=document.documentElement;
  const max=doc.scrollHeight-doc.clientHeight;
  const p=max>0?Math.min(window.scrollY/max,1):0;
  bar.style.width=(p*100)+'%';
  orbProg.style.strokeDashoffset=CIRC*(1-p);
  orbPct.textContent=Math.round(p*100)+'%';

  const mid=window.scrollY+window.innerHeight*0.35;
  let active=0;
  chapters.forEach((c,i)=>{if(c.offsetTop<=mid)active=i});
  dots.forEach((d,i)=>d.classList.toggle('active',i===active));
}
window.addEventListener('scroll',update,{passive:true});
window.addEventListener('resize',update);
update();

dots.forEach(d=>{
  d.addEventListener('click',()=>{
    document.getElementById(d.dataset.target)
      .scrollIntoView({behavior:'smooth',block:'start'});
  });
});