技術情報

カウントアップの数字が、ガタつかないようにする作り方

要点

  • 揺れる原因は、数字の幅が桁数や字形で変わること
  • 見えない最終値(ゴースト)を同じ場所に重ねて、幅を先に確保する
  • 上のデモは、左が揺れる版、右が揺れない版

なぜ揺れるのか

「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;
}
  1. inline-grid と grid-area: 1 / 1:表示する数字と、見えない最終値を同じマスに重ねます。幅は、広いほう(最終値)で決まります。
  2. justify-self: end:増えていく数字を右端に揃えます。右隣のテキストとの距離が変わりません。
  3. visibility: hidden:見えなくても幅が残ります。display: none では幅が消えます。
  4. 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。役割を分けると、数字が変わっても崩れにくくなります。