Website Animation Guides · 2026-10-07 · 5 min read
Count-Up Animation for Websites: Copyable JavaScript and Accessible Timing
A useful count-up animation starts when a real metric enters view, finishes once and keeps the final number accessible.
Written by AmJ, founder of Scrollhaus, and edited by the Scrollhaus editorial team. Sources and methodology are documented in the article.

Key takeaways
A count-up animation turns one verified metric into a short visual cue. On a website, it should begin when the number enters view, stop exactly at its target, and leave the final value readable. A wall of animated statistics usually weakens the hierarchy: choose one or two facts that matter to a visitor.
Choose a live counter or an exported animation
A DOM counter is the right format when a website number may change. It remains responsive and selectable. An MP4, GIF or Lottie file is better for a video edit or presentation, but can become inaccurate when the number changes. The example below is a plain JavaScript number-counting animation for a web page.
Copy a count-up animation in JavaScript
The counter waits until 40 percent of its element is visible, counts once over 1.2 seconds and prints the target immediately when reduced motion is preferred. Replace 1250 with a real figure. Keep the accessible label in sync with the final value.
<p>Projects shipped: <span data-count-to="1250"
aria-label="1,250 projects shipped">0</span></p>
<script>
const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
const format = new Intl.NumberFormat();
const observer = new IntersectionObserver(entries => {
for (const entry of entries) {
if (!entry.isIntersecting) continue;
const el = entry.target;
observer.unobserve(el);
const target = Number(el.dataset.countTo);
if (!Number.isFinite(target)) continue;
if (reduced) { el.textContent = format.format(target); continue; }
let start;
function tick(now) {
start ??= now;
const progress = Math.min((now - start) / 1200, 1);
const eased = 1 - (1 - progress) ** 3;
el.textContent = format.format(Math.round(target * eased));
if (progress < 1) requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
}
}, { threshold: 0.4 });
document.querySelectorAll('[data-count-to]').forEach(el => observer.observe(el));
</script>
Intersection Observer avoids running the effect above the fold when nobody has seen it. requestAnimationFrame follows the browser paint cycle. The easing slows the last increments, and unobserving the element prevents reverse scrolling from restarting the count.
Format the number and its unit
Put a visible percent symbol, currency sign or unit next to the number and say the full value in its accessible label. The sample rounds to whole numbers. If precision matters, interpolate the raw value and format only the approved decimal places. A number without context is rarely persuasive: explain what it measures, where it came from and when it was measured.
Keep the target separate from the display. If a database supplies the value, load the real data before starting. Reserve enough width for the final digit count so text beside it does not jump. Never animate a placeholder such as invented revenue or fictional customer results as though it were evidence.
React and AI-builder version
In React, create the observer inside an effect and clean up the observer and any scheduled animation frame when the component unmounts. Use a prop for the target. Do not paste a document-wide script into every React card, because duplicate observers will compete. When prompting an AI builder, request a one-time DOM counter, a static reduced-motion state, a real data source and a test for layout shift.
Compare this small effect with a complete website animation system, or explore the Scrollhaus effects collection. A counter should support a product story rather than replace one.
Verify before publishing
- Scroll down, back up and down again: the number finishes once.
- Enable reduced motion: the target appears without counting.
- Check narrow phones and longer digit groups for layout shift.
- Confirm the metric, unit, accessible label and source agree.
Implementation references: MDN Intersection Observer, MDN requestAnimationFrame and React effect cleanup.
Frequently asked questions
Can I make a count-up animation with CSS only?
CSS can reveal or move digits, but JavaScript is practical when the target value changes or needs numeric formatting.
Why does my counter restart when I scroll back?
The visibility observer is firing again. Unobserve the element after its first trigger or maintain a completed state.
Should a counter use aria-live?
Usually no. Announcing every intermediate value can interrupt screen-reader users; provide a stable accessible label with the final value.
Explore animated website templates at Scrollhaus →
Need help with a template, account, or project? Email admin@scrollhaus.ai.