06統計カード3枚の順次カウント
3枚のカードは同時に始まらず、animation-delayで0.15秒ずつずらして順番に数字が埋まっていく。
使いどころダッシュボードのサマリーカード、LPの実績3種
animation-delay0.15초 간격카드 3장

<main class="stage">
<div class="cards is-demo">
<div class="card">
<span class="card__num" aria-hidden="true"></span>
<span class="card__sr">356</span>
<span class="card__label">会員</span>
</div>
<div class="card">
<span class="card__num" aria-hidden="true"></span>
<span class="card__sr">82</span>
<span class="card__label">売上</span>
</div>
<div class="card">
<span class="card__num" aria-hidden="true"></span>
<span class="card__sr">5</span>
<span class="card__label">評価</span>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-loop;
$easing: $ease-out;
$color: $subject-ink;
@include stage($stage-yellow);
@property --num {
syntax: '<integer>';
initial-value: 0;
inherits: false;
}
.cards {
display: flex;
gap: clamp(10px, 3vw, 18px);
}
.card {
min-width: clamp(64px, 20vw, 88px);
padding: clamp(10px, 3vw, 16px) clamp(8px, 2.4vw, 12px);
border-radius: $r-card;
background: rgba(23, 20, 26, .08);
text-align: center;
&__num {
--num: 0;
counter-reset: num var(--num);
display: block;
font-weight: 800; font-size: clamp(20px, 6vw, 28px); line-height: 1; font-family: inherit;
font-variant-numeric: tabular-nums;
color: $color;
&::before { content: counter(num); }
}
&__sr {
position: absolute;
width: 1px; height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
&__label {
display: block;
margin-top: 4px;
font-weight: 600; font-size: clamp(10px, 3vw, 12px); line-height: 1; font-family: inherit;
color: rgba($subject-ink, .72); // 흐린 글자는 알파 .72 하한 — 카드 옐로 면 위 5.58:1
}
}
// 카드 세 장이 동시에 시작하지 않는다 — 순서대로 0.15초씩 늦게
.cards.is-demo .card:nth-child(1) .card__num { animation: count-a $duration $easing infinite; }
.cards.is-demo .card:nth-child(2) .card__num { animation: count-b $duration $easing infinite; animation-delay: .15s; }
.cards.is-demo .card:nth-child(3) .card__num { animation: count-c $duration $easing infinite; animation-delay: .3s; }
@keyframes count-a { 0% { --num: 0; } 55% { --num: 356; } 100% { --num: 356; } }
@keyframes count-b { 0% { --num: 0; } 55% { --num: 82; } 100% { --num: 82; } }
@keyframes count-c { 0% { --num: 0; } 55% { --num: 5; } 100% { --num: 5; } }
@media (prefers-reduced-motion: reduce) {
.cards.is-demo .card:nth-child(1) .card__num { animation: none; --num: 356; }
.cards.is-demo .card:nth-child(2) .card__num { animation: none; --num: 82; }
.cards.is-demo .card:nth-child(3) .card__num { animation: none; --num: 5; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
@property --num {
syntax: "<integer>";
initial-value: 0;
inherits: false;
}
.cards {
display: flex;
gap: clamp(10px, 3vw, 18px);
}
.card {
min-width: clamp(64px, 20vw, 88px);
padding: clamp(10px, 3vw, 16px) clamp(8px, 2.4vw, 12px);
border-radius: 16px;
background: rgba(23, 20, 26, 0.08);
text-align: center;
}
.card__num {
--num: 0;
counter-reset: num var(--num);
display: block;
font-weight: 800;
font-size: clamp(20px, 6vw, 28px);
line-height: 1;
font-family: inherit;
font-variant-numeric: tabular-nums;
color: #17141a;
}
.card__num::before {
content: counter(num);
}
.card__sr {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.card__label {
display: block;
margin-top: 4px;
font-weight: 600;
font-size: clamp(10px, 3vw, 12px);
line-height: 1;
font-family: inherit;
color: rgba(23, 20, 26, 0.72);
}
.cards.is-demo .card:nth-child(1) .card__num {
animation: count-a 2s ease-out infinite;
}
.cards.is-demo .card:nth-child(2) .card__num {
animation: count-b 2s ease-out infinite;
animation-delay: 0.15s;
}
.cards.is-demo .card:nth-child(3) .card__num {
animation: count-c 2s ease-out infinite;
animation-delay: 0.3s;
}
@keyframes count-a {
0% {
--num: 0;
}
55% {
--num: 356;
}
100% {
--num: 356;
}
}
@keyframes count-b {
0% {
--num: 0;
}
55% {
--num: 82;
}
100% {
--num: 82;
}
}
@keyframes count-c {
0% {
--num: 0;
}
55% {
--num: 5;
}
100% {
--num: 5;
}
}
@media (prefers-reduced-motion: reduce) {
.cards.is-demo .card:nth-child(1) .card__num {
animation: none;
--num: 356;
}
.cards.is-demo .card:nth-child(2) .card__num {
animation: none;
--num: 82;
}
.cards.is-demo .card:nth-child(3) .card__num {
animation: none;
--num: 5;
}
}import type { CSSProperties } from "react";
import styles from "./StatCardsStagger.module.scss";
export type Stat = { value: number; label: string };
export type StatCardsStaggerProps = {
stats: [Stat, Stat, Stat];
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & {
"--duration"?: string; "--easing"?: string; "--color"?: string;
"--num-target"?: number; "--delay"?: string;
};
export default function StatCardsStagger({
stats, duration = 2000, easing = "ease-out", color = "#17141a",
}: StatCardsStaggerProps) {
return (
<div className={styles.cards}>
{stats.map((s, i) => {
const vars: Vars = {
"--duration": `${duration}ms`,
"--easing": easing,
"--color": color,
"--num-target": s.value,
"--delay": `${i * 150}ms`, // 카드마다 0.15초씩 늦게 시작
};
return (
<div className={styles.card} key={s.label} style={vars}>
<span className={styles.num} aria-hidden="true" />
<span className={styles.sr}>{s.value.toLocaleString()}</span>
<span className={styles.label}>{s.label}</span>
</div>
);
})}
</div>
);
}06 統計カード3枚の順次カウント — 計測 累積面積 1.303% · 動いたフレーム 15/23GODRICH オリジナル · SCSS と React は zip に同梱 · CSS 数字 カウントアップ アニメーション9選 — 一行でコピペOK
このデモが属する記事
CSS 数字 カウントアップ アニメーション9選 — 一行でコピペOKcss 数字 カウントアップは、0や開始値から目標の数字まで上がっていくテキストを、JavaScriptなしでCSSのカスタムプロパティだけで数える手法です。9つのうち1つは逆に0へ向かって減っていき、別の1つはスクロールして画面に入るまで この記事のデモをすべて見る


