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

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

Share & Support

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.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Paste this snippet's JS into an AI coding assistant like Claude and ask it to explain why the "back" easing formula, 1 + c3*(t-1)^3 + c1*(t-1)^2, produces a value that exceeds 1.0 before returning to exactly 1.0, and how that directly translates into a displayed number that overshoots its target and settles — since the displayed value is simply target multiplied by that eased fraction. It's also useful for extending the effect: ask it to replace the single-overshoot "back" ease with a true damped-spring simulation (mass, stiffness, damping) that oscillates a couple of times before settling, or to make the overshoot amount scale with how large the target number is so huge numbers overshoot more dramatically than small ones.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a "scroll-triggered overshoot counter" effect in plain HTML, CSS, and vanilla JavaScript (no libraries).

Requirements:
- A grid of stat cards, each with a target number, an optional prefix (e.g. "$") and suffix (e.g. "%") stored as data attributes on a .oc-value element, starting displayed as 0.
- Use an IntersectionObserver with threshold 0.5 to detect when each stat card scrolls into view, and trigger that stat's count-up animation independently the moment it crosses the threshold — do not use a single global scroll listener for the trigger.
- Implement a genuine overshoot/spring easing function (the "back" easing formula: 1 + c3*(t-1)^3 + c1*(t-1)^2 with c3 = c1 + 1, using a c1 value like 1.7–2) rather than a linear or ease-out count-up. This easing function must evaluate to a value greater than 1.0 partway through its 0-to-1 input range, and return exactly 1.0 at input 1.0.
- Animate each counter with requestAnimationFrame, computing an elapsed-time fraction t from 0 to 1 over roughly 1200–1500ms, passing t through the overshoot easing function, and setting the displayed number to target * easedValue on every frame — so the displayed number visibly exceeds the target before correcting back down to land exactly on it.
- Format numbers with toLocaleString for thousands separators during the animation, and force the final frame to display the exact raw target value to avoid floating-point rounding drift.
- When a stat scrolls back out of view, reset its displayed value to 0 and clear its "already animated" flag so scrolling it back into view re-triggers the full overshoot animation from the start.
- Confirm the overshoot is visible by eye: the number should climb noticeably past its final value before dropping back down to settle exactly on it, not just decelerate smoothly into place.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

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

Metrics dashboards
Give KPI tiles real weight versus a linear count up.
Landing page stats
Pair with feature cards reveals.
Pricing pages
Emphasize savings or usage numbers with a springy entrance.
Investor decks (web)
Compare against number ticker for tone.
Product analytics
Highlight growth stats alongside odometer counters.
Annual report pages
Reveal year-end totals with a satisfying settle.

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.