CSS 数字 カウントアップ アニメーション9選 — 一行でコピペOK
css 数字 カウントアップは、0や開始値から目標の数字まで上がっていくテキストを、JavaScriptなしでCSSのカスタムプロパティだけで数える手法です。9つのうち1つは逆に0へ向かって減っていき、別の1つはスクロールして画面に入るまで0のまま止まっています。
自動再生 · タイルを押すとその項目へ · すべて1つのzip
- 01 @propertyで整数カウントアップ
- 02 桁区切りカンマの自動挿入
- 03 小数点フォーマットのカウントアップ
- 04 通貨記号付き金額カウントアップ
- 05 パーセントリングと数字の同期
- 06 統計カード3枚の順次カウント
- 07 画面に入ったら開始
- 08 オドメーター風ローリング数字
- 09 カウントダウンタイマー
9つは人気順ではなく、数字ひとつが画面上でたどる順番に並べました。ただ数えるだけの形(01)から、桁が増えて読みにくくなる部分を区切る形(02)、精度を足す形(03)、単位を固定する形(04)、別の視覚要素と値をそろえる形(05)、複数を時間差で並べる形(06)、いつ数え始めるかを自分で判断する形(07)、数える仕組み自体を別物に差し替えた形(08)、そして向きを逆にした形(09)という流れです。コード断片は実際に切り替わる部分だけを残し、完全なファイル一式はzipに入れました。
01@propertyで整数カウントアップ
数字が0から目標値まで上がる間、実際にアニメーションしているのは文字ではなく、@propertyで宣言した整数のカスタムプロパティです。その値をcounter()でそのまま描画するので画面には本物の桁が並び、会員登録数や訪問者数のような実績カードに向いています。
// syntax を '<integer>' にすると、0→12480 の途中も整数だけで補間される
@property --num {
syntax: '<integer>';
initial-value: 0;
inherits: false;
}
.num {
--num: 0;
counter-reset: num var(--num);
animation: count-up 2s ease-out infinite;
&::before { content: counter(num); }
}
@keyframes count-up {
0% { --num: 0; }
65% { --num: 12480; }
100% { --num: 12480; }
}
02桁区切りカンマの自動挿入
12,483のようなカンマを自動で付けるには、千の位と残りを別々の@property整数として同時に数える必要があります。残りは@counter-styleのpadで常に3桁に揃えないと「12,3」のような欠けが出るため、ここでは専用のカウンタースタイルで埋めています。
@property --k { syntax: '<integer>'; initial-value: 0; inherits: false; }
@property --n { syntax: '<integer>'; initial-value: 0; inherits: false; }
// 残り(n)を必ず3桁で埋める。足りない桁は0で埋める
@counter-style padded-3 {
system: numeric;
symbols: '0' '1' '2' '3' '4' '5' '6' '7' '8' '9';
pad: 3 '0';
}
.num {
counter-reset: k var(--k) n var(--n);
&::before { content: counter(k) "," counter(n, padded-3); }
}
03小数点フォーマットのカウントアップ
4.98のような評価値は、整数部と小数部をそれぞれ別の@propertyとして同時に動かし、小数部だけ2桁のpadで揃えてから小数点の後ろに置く形で作ります。平均の星評価や温度のように、小数点以下1〜2桁が意味を持つ値に向いています。
@counter-style padded-2 {
system: numeric;
symbols: '0' '1' '2' '3' '4' '5' '6' '7' '8' '9';
pad: 2 '0';
}
.num {
counter-reset: whole var(--whole) frac var(--frac);
&::before { content: counter(whole) "." counter(frac, padded-2); }
}
04通貨記号付き金額カウントアップ
通貨記号は::beforeで固定しておき、数字だけを::afterのcounter()でアニメーションさせると、桁数が1桁から4桁に増えても記号の位置はずれません。カート合計や決済総額のまとめカードに向いています。
.num {
counter-reset: num var(--num);
animation: count-up .6s ease-out infinite;
&::before { content: "$"; margin-right: 2px; }
&::after { content: counter(num); }
}
05パーセントリングと数字の同期
整数のカスタムプロパティひとつが、counter()で描く数字とconic-gradientの塗り角度の両方を同時に駆動するので、リングとパーセント表示がずれることはありません。目標達成率やストレージ使用率の表示に向いています。
.ring {
--p: 0;
background: conic-gradient($color calc(var(--p) * 1%), transparent 0);
animation: fill-up 2s ease-out infinite;
}
.ring__num {
counter-reset: p var(--p);
&::before { content: counter(p) "%"; }
}
06統計カード3枚の順次カウント
3枚のカードが同時に埋まると、どれから見ればいいのか分かりません。animation-delayで0.15秒ずつずらして開始させると、ダッシュボードのサマリーカードやランディングページの実績3種のような並びで順番に目が追えます。
.card:nth-child(1) .num { animation: count-a 2s ease-out infinite; }
.card:nth-child(2) .num {
animation: count-b 2s ease-out infinite;
animation-delay: .15s;
}
.card:nth-child(3) .num {
animation: count-c 2s ease-out infinite;
animation-delay: .3s;
}
07画面に入ったら開始
読み込んだ瞬間に数字が数え終わっていると、スクロールして下りてきた人はその過程を見られません。カードがビューポートに60%以上入った時だけIntersectionObserverが.is-countingを付け、それまではanimation-play-stateがpausedのまま0で止まります。
var card = document.querySelector('.card');
new IntersectionObserver(function (es) {
card.classList.toggle('is-counting', es[0].isIntersecting);
}, { threshold: .6 }).observe(card);
// card__num の初期値は animation-play-state: paused
// is-counting が付いた時だけ running に変わる
08オドメーター風ローリング数字
桁ごとに0〜9を縦に並べたストリップを用意し、目標の数字が来るまでtranslateYで動かすと、桁ごとに独立して回る機械式メーターのような見た目になります。3つ並べれば「128」のように読め、レトロ調カウンターやキオスクの整理番号表示に向いています。
.digit {
height: 44px;
overflow: hidden; // ストリップのうち1マスだけが見える
}
.strip {
animation: roll-to-8 2s cubic-bezier(.2, 1.4, .4, 1) infinite;
span { height: 44px; }
}
@keyframes roll-to-8 {
0% { transform: translateY(0); }
60%, 100% { transform: translateY(-80%); } // 9マス目(-80%)で止まる
}
09カウントダウンタイマー
@propertyの整数を目標値ではなく0へ向けて逆にカウントさせると、締切までの残り時間や在庫の減りを表現できます。同じ値で下の進捗バーの長さも一緒に縮め、後半では色も警告色へ変えて緊迫感を足しています。
@keyframes count-down {
0% { --t: 9; color: $color; }
60% { --t: 3; color: $color; }
85% { --t: 0; color: $error; }
100% { --t: 0; color: $error; }
}
.bar::after {
transform: scaleX(calc(var(--t) / 9)); // 同じ --t でバーも縮む
}
どこで壊れるか — 落とし穴3つ
1つ目は対応ブラウザの幅です。09までのうち08を除く8つは@propertyに依存しています。@property自体はChrome・Edgeが85以降、Safariが16.4以降、Firefoxが128以降の対応で(caniuseの@property対応表)、それより古いブラウザではアニメーションが起きず最終値がいきなり表示されるだけでレイアウトは崩れませんが、数えている感覚そのものが消えます。
2つ目は、counter()で描いた数字が本物のテキストではないという点です。ドラッグしても選択できず、スクリーンリーダーも読み上げないとMDNの生成コンテンツの文書に明記されています。そのためこのzipに入れた9つのデモはどれも、見た目には隠しつつ本物の数字テキストを別要素として一つずつ用意してあり、その対策込みの9ファイルを開く圧縮パスワードはj9xejanyで、すぐ下のカードから取得できます。
3つ目は07のような「画面に入ったら数える」仕組みでよく踏む再生の罠です。IntersectionObserverでクラスを付け外しし、animation-fill-mode: forwardsだけに任せると、一度数え終わったカードは画面外に出てまた戻ってきても二度と再生されません。07はその代わりにanimation-play-state: pausedを初期値にし、.is-countingが付いている間だけrunningへ切り替える作りにしたので、カードが視界を出入りするたびに一時停止と再開を繰り返すだけで済んでいます。
アクセシビリティ
9つともprefers-reduced-motion: reduceでは途中経過を飛ばし、animation: noneとともに最終値がそのまま置き換わります。数字が消えるのではなく、数える動きだけが抜け落ちる形です。09のカウントダウンは色まで警告色に切り替わった状態で止まるので、動きが止まっても緊迫感の情報自体は残ります。counter()生成の数字はスクリーンリーダーが読めないため、9つとも視覚的に隠した実テキストのspanを別に添えてあります。
スクロールをきっかけに動く別の仕組みはCSSスクロールアニメーション9選にまとめています。このサイトが扱うCSS素材の一覧はCSSアニメーションのカテゴリーから、運営については紹介ページからご覧ください。
FAQ
数字カウンターにJavaScriptは必須ですか?
いいえ。9つとも数える計算そのものは@propertyとCSSのアニメーション規則だけで完結します。例外は07で、いつ数え始めるかを判定するためだけにIntersectionObserverを数行添えていますが、数値を増減させる計算はCSSの外に出ていません。導入前にcaniuseの@property対応表は確認しておくと安心です。
counter()で描いた数字がコピーできないのは問題になりませんか?
画面に見せるだけの用途なら問題ありません。ただし価格表のようにユーザーが数字自体をコピーする場面では、このzipに同梱した非表示の実テキストspanを表示側に切り替えて使ってください。counter()の生成コンテンツはテキストノードとして扱われないため、選択やコピーの対象になりません。
08のオドメーターは桁数を増やしても使えますか?
はい。08のストリップは0〜9の10個のspanを前提にtranslateYの到達値を計算しているので、桁を増やす場合はdigit要素と対応するkeyframesを同じ考え方で増やしてください。react/フォルダのコンポーネントは桁の配列の長さに合わせて列の数が自動で増える作りにしてあります。