Langkau ke kandungan
WANCODE
EN

Kaunter Beranimasi

Kira nombor naik dengan lancar menggunakan requestAnimationFrame dan easing, mula bila di-scroll, dan hormati prefers-reduced-motion.

Pratonton langsung Buka dalam Playground
counter.js
/**
 * Counts an element up from 0 to its data-target value.
 * Respects prefers-reduced-motion by jumping straight to the end.
 */
export function animateCounter(el, { duration = 1500 } = {}) {
  const target = Number(el.dataset.target);
  const format = new Intl.NumberFormat(document.documentElement.lang || undefined);
  const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

  if (reduceMotion || duration <= 0) {
    el.textContent = format.format(target);
    return;
  }

  const easeOutCubic = (t) => 1 - Math.pow(1 - t, 3);
  const start = performance.now();

  function tick(now) {
    const progress = Math.min((now - start) / duration, 1);
    el.textContent = format.format(Math.round(target * easeOutCubic(progress)));
    if (progress < 1) requestAnimationFrame(tick);
  }

  requestAnimationFrame(tick);
}

/** Start each counter the first time it scrolls into view. */
export function animateCountersOnScroll(selector = "[data-target]") {
  const observer = new IntersectionObserver((entries) => {
    for (const entry of entries) {
      if (!entry.isIntersecting) continue;
      animateCounter(entry.target);
      observer.unobserve(entry.target); // only once
    }
  }, { threshold: 0.5 });

  document.querySelectorAll(selector).forEach((el) => observer.observe(el));
}

Animasi kaunter cuma begini: pada setiap frame, kira sejauh mana animasi sudah berjalan (0 → 1), dan paparkan pecahan itu daripada nombor sasaran. requestAnimationFrame memanggil kita sejurus sebelum pelayar melukis, jadi animasi lancar dan berhenti sendiri dalam tab latar belakang.

Baris demi baris

  • Intl.NumberFormat tambah pemisah ribuan mengikut bahasa halaman (12,500), tanpa helah string buatan sendiri.
  • matchMedia("(prefers-reduced-motion: reduce)") baca tetapan sistem operasi pengguna. Jika mereka minta kurangkan pergerakan, kita terus paparkan nombor akhir — maklumat sama, tiada pergerakan.
  • progress ialah masa berlalu ÷ tempoh, dihadkan kepada 1 dengan Math.min. Guna masa sebenar (bukan kiraan frame) pastikan tempoh tepat sama ada skrin 60 Hz atau 120 Hz.
  • easeOutCubic bermula laju dan perlahan menjelang akhir, jadi terasa semula jadi. Tukar kepada (t) => t untuk lihat pergerakan linear.
  • IntersectionObserver beritahu kita bila elemen sekurang-kurangnya separuh kelihatan, jadi animasi dimainkan bila orang benar-benar boleh nampak — kemudian kita unobserve supaya ia berjalan sekali sahaja.

Pembaca skrin

Nombor yang berubah 60 kali sesaat hanyalah bunyi bising bagi pembaca skrin. Letak aria-hidden="true" pada nombor beranimasi dan tambah nilai akhir dalam teks visually-hidden di sebelahnya, seperti contoh penggunaan.

Cara guna

HTML
<p class="stat">
  <span data-target="12500" aria-hidden="true">0</span>
  <span class="visually-hidden">12,500</span>
  learners
</p>

<script type="module">
  import { animateCountersOnScroll } from "./counter.js";
  animateCountersOnScroll();
</script>