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
/**
* 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.NumberFormattambah 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.progressialah masa berlalu ÷ tempoh, dihadkan kepada 1 denganMath.min. Guna masa sebenar (bukan kiraan frame) pastikan tempoh tepat sama ada skrin 60 Hz atau 120 Hz.easeOutCubicbermula laju dan perlahan menjelang akhir, jadi terasa semula jadi. Tukar kepada(t) => tuntuk lihat pergerakan linear.IntersectionObserverberitahu kita bila elemen sekurang-kurangnya separuh kelihatan, jadi animasi dimainkan bila orang benar-benar boleh nampak — kemudian kitaunobservesupaya 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
<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>