05Bộ đếm số liệu
Con số lớn trên ba card chạy từ 0 lên đúng giá trị mục tiêu ngay khi tải xong rồi dừng hẳn, còn vòng tròn nền vẫn pulse dịu liên tục.
Dùng choSection 'nhìn bằng con số' trên trang giới thiệu công ty, điểm nhấn thành tích
requestAnimationFramecounteropacity

<main class="stage">
<section class="sc">
<div class="sc__card">
<p class="sc__num" data-target="128">0</p>
<p class="sc__label">Số lần lên báo</p>
</div>
<div class="sc__card">
<p class="sc__num" data-target="42">0</p>
<p class="sc__label">Số đầu báo hợp tác</p>
</div>
<div class="sc__card">
<p class="sc__num" data-target="3200">0</p>
<p class="sc__label">Tổng lượt tải</p>
</div>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-enter; // 카드 악센트 펄스
$easing: $ease-spring;
$color: $subject-blue; // 숫자·악센트 색
@include stage($stage-ink);
.sc {
display: flex;
gap: $sp-3;
width: min(420px, 94vw);
}
.sc__card {
position: relative;
flex: 0 0 auto;
box-sizing: border-box;
width: calc((100% - #{$sp-3} * 2) / 3);
overflow: hidden;
border-radius: $r-card;
padding: $sp-4 $sp-2 $sp-3;
background: rgba(255, 247, 230, .06);
text-align: center;
}
.sc__card::before {
content: "";
position: absolute; left: 0; right: 0; top: 0;
height: 3px;
background: $color;
transform-origin: center;
animation: sc-pulse 2s $easing infinite;
}
.sc__card:nth-child(1)::before { animation-delay: 0ms; }
.sc__card:nth-child(2)::before { animation-delay: 220ms; }
.sc__card:nth-child(3)::before { animation-delay: 440ms; }
@keyframes sc-pulse {
0%, 70%, 100% { transform: scaleX(.3); opacity: .5; }
30% { transform: scaleX(1); opacity: 1; }
}
.sc__num {
margin: 0 0 $sp-1;
font-weight: 800; font-size: clamp(20px, 7vw, 28px); line-height: 1; font-family: inherit;
color: $subject-cream;
font-variant-numeric: tabular-nums;
}
.sc__label {
margin: 0;
font-weight: 600; font-size: clamp(9px, 2.6vw, 10px); line-height: 1.3; font-family: inherit;
color: rgba(255, 247, 230, .58);
}
// ── 반응형(BD9) ──────────────────────────────────────────
@media (max-width: 340px), (max-height: 220px) {
.sc { gap: $sp-2; }
.sc__card { padding: $sp-2 $sp-1; }
.sc__num { font-size: 16px; }
.sc__label { font-size: 8px; }
}
// ── 접근성 ──────────────────────────────────────────
@media (prefers-reduced-motion: reduce) {
.sc__card::before { animation: none !important; transform: scaleX(1); opacity: 1; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.sc {
display: flex;
gap: 12px;
width: min(420px, 94vw);
}
.sc__card {
position: relative;
flex: 0 0 auto;
box-sizing: border-box;
width: calc((100% - 12px * 2) / 3);
overflow: hidden;
border-radius: 16px;
padding: 16px 8px 12px;
background: rgba(255, 247, 230, 0.06);
text-align: center;
}
.sc__card::before {
content: "";
position: absolute;
left: 0;
right: 0;
top: 0;
height: 3px;
background: #2f6df6;
transform-origin: center;
animation: sc-pulse 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.sc__card:nth-child(1)::before {
animation-delay: 0ms;
}
.sc__card:nth-child(2)::before {
animation-delay: 220ms;
}
.sc__card:nth-child(3)::before {
animation-delay: 440ms;
}
@keyframes sc-pulse {
0%, 70%, 100% {
transform: scaleX(0.3);
opacity: 0.5;
}
30% {
transform: scaleX(1);
opacity: 1;
}
}
.sc__num {
margin: 0 0 4px;
font-weight: 800;
font-size: clamp(20px, 7vw, 28px);
line-height: 1;
font-family: inherit;
color: #fff7e6;
font-variant-numeric: tabular-nums;
}
.sc__label {
margin: 0;
font-weight: 600;
font-size: clamp(9px, 2.6vw, 10px);
line-height: 1.3;
font-family: inherit;
color: rgba(255, 247, 230, 0.58);
}
@media (max-width: 340px), (max-height: 220px) {
.sc {
gap: 8px;
}
.sc__card {
padding: 8px 4px;
}
.sc__num {
font-size: 16px;
}
.sc__label {
font-size: 8px;
}
}
@media (prefers-reduced-motion: reduce) {
.sc__card::before {
animation: none !important;
transform: scaleX(1);
opacity: 1;
}
}// 실제 동작: 페이지가 열리면 0에서 실제 목표값까지 숫자가 실시간으로 올라간다.
var nums = document.querySelectorAll('.sc__num');
var countStart = performance.now();
var countDuration = 1200;
function easeOut(t) { return 1 - Math.pow(1 - t, 3); }
function tick(now) {
var t = Math.min(1, (now - countStart) / countDuration);
var e = easeOut(t);
nums.forEach(function (el) {
var target = Number(el.getAttribute('data-target'));
el.textContent = Math.round(target * e).toLocaleString('ko-KR');
});
if (t < 1) requestAnimationFrame(tick);
}
requestAnimationFrame(tick);import { useEffect, useState } from "react";
import type { CSSProperties } from "react";
import styles from "./PressStatCounter.module.scss";
export type PressStatCounterProps = { duration?: number; easing?: string; color?: string };
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const STATS = [
{ target: 128, label: "언론 보도 건수" },
{ target: 42, label: "협력 매체 수" },
{ target: 3200, label: "누적 다운로드" },
];
export default function PressStatCounter({ duration = 600, easing = "cubic-bezier(.2,.8,.2,1)", color = "#2f6df6" }: PressStatCounterProps) {
const [values, setValues] = useState<number[]>(STATS.map(() => 0));
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
useEffect(() => {
const start = performance.now();
const length = 1200;
let frame: number;
function easeOut(t: number) { return 1 - Math.pow(1 - t, 3); }
function tick(now: number) {
const t = Math.min(1, (now - start) / length);
const e = easeOut(t);
setValues(STATS.map((s) => Math.round(s.target * e)));
if (t < 1) frame = requestAnimationFrame(tick);
}
frame = requestAnimationFrame(tick);
return () => cancelAnimationFrame(frame);
}, []);
return (
<section className={styles.sc} style={vars}>
{STATS.map((s, i) => (
<div key={s.label} className={styles.card}>
<p className={styles.num}>{values[i].toLocaleString("ko-KR")}</p>
<p className={styles.label}>{s.label}</p>
</div>
))}
</section>
);
}05 Bộ đếm số liệu — Diện tích đo được 2.675% · khung hình chuyển động 21/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · 9 mẫu press page design — dán vào landing
Bài viết chứa demo này
9 mẫu press page design — dán vào landingPress page design là bộ code gom sẵn từng khối của trang báo chí: press release, media kit, liên hệ người phụ trách. Từ Xem tất cả demo trong bài này


