webdevpuneet.com

Scroll-Triggered Overshoot Counter — Free Spring Count-Up Effect

Scroll-Triggered Overshoot Counter · Scroll · Plain HTML, CSS & JS · Live preview

Copy & paste

Source Code

<section class="oc-intro"><h1>Scroll ↓</h1><p>Each number overshoots its target, then springs back to settle exactly on it.</p></section>
<section class="oc-grid" id="ocGrid">
  <div class="oc-stat"><div class="oc-value" data-target="128400" data-prefix="$" data-suffix="">0</div><div class="oc-label">Revenue this quarter</div></div>
  <div class="oc-stat"><div class="oc-value" data-target="4820" data-prefix="" data-suffix="">0</div><div class="oc-label">Active subscribers</div></div>
  <div class="oc-stat"><div class="oc-value" data-target="97" data-prefix="" data-suffix="%">0</div><div class="oc-label">Uptime this month</div></div>
  <div class="oc-stat"><div class="oc-value" data-target="312" data-prefix="" data-suffix="">0</div><div class="oc-label">New signups today</div></div>
</section>
<section class="oc-outro"><p>Scroll back up and re-enter — it springs again.</p></section>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), 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

Real overshoot easing
A back-ease formula that genuinely exceeds 1 mid-animation.
Settles exactly on target
Eased value returns to 1.0 precisely at animation end.
IntersectionObserver trigger
Cheap, per-stat visibility detection, no scroll math needed.
Independent per-stat timing
Each stat runs its own rAF loop from its own start time.
Re-arms on scroll-back
Leaving and re-entering resets and replays the spring.
Data-attribute config
Target, prefix, suffix all set from markup, zero JS edits.
Locale-formatted digits
toLocaleString adds thousands separators while animating.
No dependencies
Pure vanilla JS and IntersectionObserver, no library needed.

About this UI Snippet

Scroll-Triggered Overshoot Counter — Numbers That Overshoot, Then Settle

Screenshot of the Scroll-Triggered Overshoot Counter snippet rendered live

A plain count-up animates linearly from zero to a target and stops. This one does something a real spring does: it runs past the target, then corrects back down to land exactly on the right number — the kind of motion you'd feel from a physical needle or a UI element with real weight. It's built in vanilla JavaScript with an IntersectionObserver trigger and a genuine overshoot easing function, no animation library required.

A real overshoot formula, not a trick

The easing is the classic "back" curve, 1 + c3 * (t-1)^3 + c1 * (t-1)^2, tuned with c1 = 1.9 so the eased value climbs past 1.0 partway through the animation before descending back to exactly 1.0 at t = 1. Multiplying the target value by this eased fraction means the displayed number genuinely exceeds the target mid-animation and then comes back down — this is verifiable by watching the number tick past its final value before settling, not a fixed one-time bounce applied after the fact.

IntersectionObserver, not a scroll listener

Because the trigger only needs to know "has this stat scrolled into view," a single IntersectionObserver with threshold: 0.5 is simpler and cheaper than measuring scroll position on every frame. Each stat animates independently the moment half of it is visible, using its own requestAnimationFrame loop driven by elapsed time and the overshoot easing function.

Resets so it can spring again

When a stat scrolls back out of view, its data-animated flag clears and its display resets to zero. Scrolling it back into view re-triggers the full overshoot-and-settle animation from scratch, which is useful for demoing the effect repeatedly and keeps the section feeling alive rather than "already spent" on a second pass.

Per-stat configuration via data attributes

Each .oc-value reads its target number, prefix, and suffix straight from data-target, data-prefix, and data-suffix attributes, so adding a new stat to the grid is just a markup change — no JS edits needed. Numbers format with toLocaleString for thousands separators as they animate, not only at the end.

Customizing it

Tune c1 for a bigger or smaller overshoot, change duration for a snappier or slower spring, or swap the "back" formula for a true damped-spring simulation if you want multiple decaying oscillations instead of a single overshoot. Pair it with a scroll reveal grid, or compare it against a linear count up and odometer stat counter to see the difference in feel.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSAn intro, a stat grid, and an outro render — no dependencies.
  2. 2
    Scroll the stat grid into viewEach number counts up, overshoots, then springs back to settle.
  3. 3
    Watch the peakNumbers briefly exceed their final value before correcting down.
  4. 4
    Scroll away and backThe stat resets to zero and re-triggers the spring on re-entry.
  5. 5
    Add a statAdd a .oc-stat with data-target, data-prefix, data-suffix.
  6. 6
    Tune the springAdjust c1 for overshoot amount, duration for speed.

Real-world uses

Common Use Cases

Weighty KPI tiles
Give dashboard figures a springy feel, running past the target and then settling exactly on it through a back-ease that genuinely exceeds 1 mid-animation.
Landing page stats
Pair with a scroll reveal grid of feature cards so the statistics bounce in after the cards appear.
Savings and usage numbers
Emphasise a saving or usage figure on a pricing page, with each stat running its own animation loop from its own start time.
Plain count-up comparison
Compare against count up and number ticker to choose between linear, odometer and spring-like motion for the same data.
Annual report totals
Reveal year-end totals with a satisfying settle, as a contrast to the mechanical roll of an odometer stat counter.
Related: Scroll Comic Panel Sequence
See the Scroll Comic Panel Sequence for a related scroll pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

The eased fraction itself exceeds 1.0 partway through the animation, using the "back" easing formula 1 + c3*(t-1)^3 + c1*(t-1)^2. Since the displayed number is target * easedFraction, when easedFraction is greater than 1 the displayed number is genuinely larger than the target — it's real math producing the overshoot, not a separate bounce animation layered on top.

Yes. The easing formula is constructed so it evaluates to exactly 1.0 at t = 1 (the end of the animation), and the code also forces the final displayed value to the raw target on the last frame to avoid any floating-point rounding drift, so it always settles precisely on the number in data-target.

The effect only needs a one-time trigger — "this stat is now visible" — not a continuous scroll-position value. IntersectionObserver reports exactly that with threshold: 0.5, is cheaper than reading getBoundingClientRect on every scroll event, and automatically handles resizing and layout changes without extra code.

Yes — increase c1 in easeOutBackOvershoot for a larger, more dramatic overshoot, or lower it toward 0 for a subtler settle that barely exceeds the target. You can also shorten or lengthen the duration variable to make the whole spring feel snappier or slower.

Render the stat markup with the same data attributes, then in a mount effect create the IntersectionObserver scoped to a container ref and observe each stat element found within it. Keep the animateValue and easing functions as plain utilities. Return a cleanup that disconnects the observer on unmount.