webdevpuneet.com

Sticky Section Counter — Native CSS scroll-timeline

Sticky Section Counter (scroll-timeline) · Scroll · Plain HTML, CSS & JS · Live preview

Copy & paste

Source Code

<div class="stc-page">
  <aside class="stc-counter" aria-hidden="true">
    <span class="stc-num">01</span>
    <span class="stc-total">/ 05</span>
    <div class="stc-track"><div class="stc-fill"></div></div>
  </aside>
  <main class="stc-sections">
    <section class="stc-section" style="--c:#6366f1"><span class="stc-tag">01</span><h2>Discover</h2><p>Users find the product through search, referral, or a shared link — the first ten seconds decide whether they stay.</p></section>
    <section class="stc-section" style="--c:#0891b2"><span class="stc-tag">02</span><h2>Evaluate</h2><p>They compare pricing, features, and reviews across three or four tabs before committing to a trial.</p></section>
    <section class="stc-section" style="--c:#16a34a"><span class="stc-tag">03</span><h2>Onboard</h2><p>A guided first session determines whether the trial converts into daily habitual use.</p></section>
    <section class="stc-section" style="--c:#ea580c"><span class="stc-tag">04</span><h2>Adopt</h2><p>Regular use across the team turns a single seat into a company-wide rollout.</p></section>
    <section class="stc-section" style="--c:#db2777"><span class="stc-tag">05</span><h2>Advocate</h2><p>Satisfied teams refer the product onward, closing the loop back to discovery.</p></section>
  </main>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Native CSS counter (counter-reset / counter-increment / content: counter()) — not a JS-written number
steps(4) timing function forces clean, discrete integer jumps, never fractional blending
animation-timeline: scroll(root) drives both the counter and the vertical fill track
position: sticky keeps the whole counter pinned during the section scroll
Zero JavaScript scroll or intersection handling for the number itself
Responsive fallback collapses to a horizontal, non-sticky layout on narrow viewports
@supports fallback shows a static dash instead of a counter frozen at 01
Easily restyled with a custom @counter-style for roman numerals or letters

About this UI Snippet

Sticky Section Counter — CSS Counters Driven by animation-timeline: scroll()

Screenshot of the Sticky Section Counter (scroll-timeline) snippet rendered live

A sticky section counter — the small "02 / 05" indicator pinned beside long-form content on editorial sites and portfolios — normally requires JavaScript: an IntersectionObserver or scroll listener tracking which section is currently in view and writing a number into the DOM. This snippet reproduces the same effect using only position: sticky, native CSS counters, and animation-timeline: scroll(root) — the number in the corner is never written by JavaScript at all.

Sticky positioning does the pinning

.stc-counter uses position: sticky; top: 0; height: 100vh, so it stays pinned in the viewport as .stc-sections scrolls past beside it — this part is ordinary CSS with no scroll-driven animation involved, the same technique behind Sticky Sidebar.

A CSS counter driven by a scroll-bound keyframe animation

The actual number is a native CSS counter, initialized with counter-reset: sec-count 1 and rendered via content: counter(sec-count, decimal-leading-zero) on a ::before pseudo-element — this is the same mechanism browsers use for ordered-list numbering. What makes it scroll-driven is that a @keyframes animation on the counter's own element increments counter-increment at four evenly-spaced keyframe stops, using steps(4) as the animation's timing function so each stop is instant rather than eased. Binding that animation to animation-timeline: scroll(root) means the browser fires each counter-increment step at the corresponding point in the page's total scroll range — 25% of the way down the page, the counter jumps from 01 to 02, and so on.

Why steps() instead of linear

A linear timing function would blend between counter values in a way that makes no visual sense for a whole number — CSS counters do not interpolate fractionally. steps(4) forces the animation to jump discretely between its four increment stops, so the displayed number is always a clean integer, never a blended or rounded fractional counter value.

The accompanying fill track

A slim vertical track beside the number fills from 0% to 100% height using the exact same animation-timeline: scroll(root) binding, giving a continuous secondary progress cue alongside the discrete counter — similar in spirit to Scroll Timeline Dots but rendered as a single continuous bar instead of individual dot markers.

A structural limitation worth knowing

Because the counter increments are spaced evenly across the total scroll range (25%, 50%, 75%, 100%), this technique assumes all five sections are roughly equal height. If sections vary dramatically in length, the counter will drift out of sync with which section is actually centered in the viewport — an IntersectionObserver-based counter tracks the DOM directly and does not have this limitation, at the cost of requiring JavaScript.

Browser support

Chromium-based browsers (Chrome, Edge, Opera, Brave) support animation-timeline: scroll() today. Firefox and Safari support is still landing, so an @supports not (animation-timeline: scroll()) block swaps the counter for a static em dash and fills the track completely rather than leaving it stuck at 01.

Customizing it

Adjust the keyframe percentages in stc-increment to match your actual section proportions, add more counter-increment stops for additional sections, or swap the decimal-leading-zero counter style for upper-roman or a custom @counter-style for a different numeral system. Pair it with Scroll Spy Nav for a version that also highlights the matching nav link.

Step by step

How to Use

  1. 1
    Paste the HTML, CSS, and JSA sticky counter beside five full-height sections renders immediately.
  2. 2
    Scroll through the sectionsWatch the counter step from 01 to 05 and the vertical track fill as you pass each section boundary.
  3. 3
    Match your real section countUpdate counter-reset and add or remove counter-increment stops in @keyframes stc-increment to match your section count.
  4. 4
    Align stops to uneven sectionsIf sections vary in height, adjust the keyframe percentages so each stop lands where that section actually begins.
  5. 5
    Change the numeral styleSwap decimal-leading-zero in the counter() function for upper-roman, or define a custom @counter-style.
  6. 6
    Export in your formatClick "HTML" for a standalone file, "JSX" for a React component, or "Tailwind" for a React + Tailwind version.

Real-world uses

Common Use Cases

Editorial and long-form storytelling pages
Pair with a Scroll Company Timeline or narrative sequence so readers always know which chapter they are in.
Portfolio case-study pages
Number each project phase and keep the count visible without a JS scroll listener.
Learn CSS counters plus scroll-timeline
A focused demo of combining native CSS counters with animation-timeline for a non-obvious effect pairing.
Multi-step explainer or how-it-works sections
Show progress through a fixed sequence of steps as the reader scrolls, similar in spirit to Scroll Pin Steps.
Replace an IntersectionObserver section tracker
Removes the need for scroll-listener-based active-section tracking when sections are roughly equal height.
Related: Scroll Timeline Dots
See the Scroll Timeline Dots for a related native scroll-timeline milestone-marker pattern.
Related: Heading Underline Fill (view-timeline)
See the Heading Underline Fill (view-timeline) for a related scroll pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

The counter is initialized with counter-reset: sec-count 1 and displayed via content: counter(sec-count, decimal-leading-zero) on a pseudo-element — a native CSS feature normally used for ordered-list numbering. A @keyframes animation increments counter-increment at fixed percentage stops, and binding that animation to animation-timeline: scroll(root) ties those stops to specific points in the page's scroll range instead of to elapsed time.

CSS counters hold whole numbers and cannot interpolate fractionally between them. steps(4) forces the animation to jump discretely between its four increment stops rather than trying to blend, keeping the displayed number always a clean integer.

Because the keyframe stops are spaced evenly across the total scroll range (every 25%), the counter assumes roughly equal-height sections. If your sections vary a lot, either adjust each keyframe percentage to match your actual section proportions, or fall back to an IntersectionObserver-based counter that reads the DOM directly and is unaffected by uneven heights.

Yes. Because the animation is bound to a live scroll(root) timeline rather than triggered once, scrolling back up moves the animation's playback position backward and the counter-increment steps apply in reverse, decrementing the displayed number as expected.

The @supports not (animation-timeline: scroll()) block replaces the counter's content with a static em dash and fills the track fully, so Firefox and Safari users see a stable placeholder rather than a counter stuck at 01 or a track stuck empty.

Yes. Increase counter-reset's implicit total sections by adding matching entries to @keyframes stc-increment (for example a stop at every 1/N of 100% for N sections) and update the JS-driven "/ 05" total text, which is computed automatically from the number of .stc-section elements in the DOM.