カウントアップの数字が、ガタつかないようにする作り方
要点
- 揺れる原因は、数字の幅が桁数や字形で変わること
- 見えない最終値(ゴースト)を同じ場所に重ねて、幅を先に確保する
- 上のデモは、左が揺れる版、右が揺れない版
なぜ揺れるのか
「1」は細く、「0」は太く表示されます。数字の幅が変わるたびに、右に並んだテキストが押されて、揺れて見えます。
考え方:最終値の幅を、先に確保する
<span class="count">
<span class="count__value">150</span>
<span class="count__ghost" aria-hidden="true">150</span>
</span>件以上
.count {
display: inline-grid;
font-variant-numeric: tabular-nums;
}
.count__value,
.count__ghost {
grid-area: 1 / 1;
}
.count__value {
justify-self: end;
}
.count__ghost {
visibility: hidden;
}
inline-gridとgrid-area: 1 / 1:表示する数字と、見えない最終値を同じマスに重ねます。幅は、広いほう(最終値)で決まります。justify-self: end:増えていく数字を右端に揃えます。右隣のテキストとの距離が変わりません。visibility: hidden:見えなくても幅が残ります。display: noneでは幅が消えます。font-variant-numeric: tabular-nums:数字の幅をそろえて、細かい揺れも抑えます。
JavaScript
requestAnimationFrame で、イージング(最初に速く、最後にゆっくり)をかけるだけです。画面に入ったときに始めるため、IntersectionObserver を使います。
const DURATION = 1600;
const easeOutCubic = (t) => 1 - Math.pow(1 - t, 3);
const run = (counter) => {
const value = counter.querySelector(".count__value");
const target = Number(counter.dataset.countTo);
const startAt = performance.now();
const tick = (now) => {
const progress = Math.min(1, Math.max(0, (now - startAt) / DURATION));
value.textContent = String(Math.round(target * easeOutCubic(progress)));
if (progress < 1) window.requestAnimationFrame(tick);
};
window.requestAnimationFrame(tick);
};
気をつけたこと
- ゴーストの桁数は、最終値と同じにします。「150」まで増えるなら、ゴーストも「150」です。
- HTML には、最初から最終値を書いておきます。JavaScript が動かなくても、正しい数字が出ます。0 に戻して数えるのは、動くときだけです。
- ゴーストには
aria-hiddenを付けます。同じ数字が二重に読み上げられるのを防ぎます。 - 動きを減らす設定には、カウントアップをせず、最終値をそのまま出します。
幅を決めるのは CSS、数字を動かすのは JavaScript。役割を分けると、数字が変わっても崩れにくくなります。