04Social proof strip
겹친 아바타 세 개가 은은하게 맥박치는 동안, 큰 숫자가 로드되자마자 0에서 실제 목표값(2,431)까지 requestAnimationFrame으로 올라갑니다.
Best for
requestAnimationFramecountertransform

<main class="stage">
<section class="sp">
<div class="sp__avatars" aria-hidden="true">
<span class="sp__avatar"></span>
<span class="sp__avatar"></span>
<span class="sp__avatar"></span>
<span class="sp__avatar sp__avatar--more">+</span>
</div>
<p class="sp__count"><span class="sp__num" data-target="2431">0</span>joined</p>
<p class="sp__label">Early birds already in</p>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-enter; // 카운트업 대역(장식 펄스 기준)
$easing: $ease-spring;
$color: $subject-blue;
@include stage($stage-paper);
.sp { width: min(320px, 94vw); text-align: center; }
.sp__avatars { display: flex; justify-content: center; margin-bottom: $sp-3; }
.sp__avatar {
width: 40px; height: 40px;
border-radius: 50%;
background: $subject-blue;
border: 3px solid $stage-paper;
margin-left: -$sp-3;
box-shadow: $shadow-press;
animation: sp-pulse 2.4s ease-in-out infinite;
}
.sp__avatar:first-child { margin-left: 0; }
.sp__avatar:nth-child(2) { background: $subject-ink; animation-delay: -.6s; }
.sp__avatar:nth-child(3) { background: #dcc2b7; animation-delay: -1.2s; }
.sp__avatar--more {
display: grid; place-items: center;
background: rgba(23, 20, 26, .08);
color: $subject-ink;
font-weight: 700; font-size: 12px; line-height: 1; font-family: inherit;
animation-delay: -1.8s;
}
@keyframes sp-pulse {
0%, 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(47, 109, 246, 0); }
50% { transform: scale(1.07); box-shadow: 0 0 0 4px rgba(47, 109, 246, .18); }
}
.sp__count { margin: 0; font-weight: 800; font-size: clamp(22px, 8vw, 30px); line-height: 1.1; font-family: inherit; color: $subject-ink; }
.sp__num { font-variant-numeric: tabular-nums; color: $subject-blue; }
.sp__label { margin: $sp-1 0 0; font-weight: 600; font-size: 12px; line-height: 1.3; font-family: inherit; color: rgba(23, 20, 26, .72); } // AA 4.5 (blog-design 글자 대비)
// ── 접근성 ──────────────────────────────────────────
@media (prefers-reduced-motion: reduce) {
.sp__avatar { animation: none !important; }
}
// ── 반응형(320×200) ──────────────────────────────────
@media (max-width: 340px), (max-height: 220px) {
.sp__avatar { width: 32px; height: 32px; }
.sp__avatars { margin-bottom: $sp-2; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.sp {
width: min(320px, 94vw);
text-align: center;
}
.sp__avatars {
display: flex;
justify-content: center;
margin-bottom: 12px;
}
.sp__avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: #2f6df6;
border: 3px solid #ffffff;
margin-left: -12px;
box-shadow: 0 6px 12px rgba(23, 20, 26, 0.32);
animation: sp-pulse 2.4s ease-in-out infinite;
}
.sp__avatar:first-child {
margin-left: 0;
}
.sp__avatar:nth-child(2) {
background: #17141a;
animation-delay: -0.6s;
}
.sp__avatar:nth-child(3) {
background: #dcc2b7;
animation-delay: -1.2s;
}
.sp__avatar--more {
display: grid;
place-items: center;
background: rgba(23, 20, 26, 0.08);
color: #17141a;
font-weight: 700;
font-size: 12px;
line-height: 1;
font-family: inherit;
animation-delay: -1.8s;
}
@keyframes sp-pulse {
0%, 100% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(47, 109, 246, 0);
}
50% {
transform: scale(1.07);
box-shadow: 0 0 0 4px rgba(47, 109, 246, 0.18);
}
}
.sp__count {
margin: 0;
font-weight: 800;
font-size: clamp(22px, 8vw, 30px);
line-height: 1.1;
font-family: inherit;
color: #17141a;
}
.sp__num {
font-variant-numeric: tabular-nums;
color: #2f6df6;
}
.sp__label {
margin: 4px 0 0;
font-weight: 600;
font-size: 12px;
line-height: 1.3;
font-family: inherit;
color: rgba(23, 20, 26, 0.72);
}
@media (prefers-reduced-motion: reduce) {
.sp__avatar {
animation: none !important;
}
}
@media (max-width: 340px), (max-height: 220px) {
.sp__avatar {
width: 32px;
height: 32px;
}
.sp__avatars {
margin-bottom: 8px;
}
}// 실제 동작: 페이지가 뜨자마자 0 → 실제 목표값(2,431)까지 requestAnimationFrame 으로 실측 카운트업된다.
var el = document.querySelector('.sp__num');
var target = Number(el.getAttribute('data-target'));
var start = null;
var DUR = 1200;
function frame(ts) {
if (!start) start = ts;
var p = Math.min(1, (ts - start) / DUR);
var eased = 1 - Math.pow(1 - p, 3);
el.textContent = Math.round(eased * target).toLocaleString('ko-KR');
if (p < 1) requestAnimationFrame(frame);
}
requestAnimationFrame(frame);import { useEffect, useRef, useState } from "react";
import type { CSSProperties } from "react";
import styles from "./SocialProof.module.scss";
export type SocialProofProps = { duration?: number; easing?: string; color?: string; target?: number };
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function SocialProof({ duration = 600, easing = "cubic-bezier(.2,.8,.2,1)", color = "#2f6df6", target = 2431 }: SocialProofProps) {
const [count, setCount] = useState(0);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
const startRef = useRef<number | null>(null);
useEffect(() => {
let raf = 0;
const DUR = 1200;
function frame(ts: number) {
if (startRef.current === null) startRef.current = ts;
const p = Math.min(1, (ts - startRef.current) / DUR);
const eased = 1 - Math.pow(1 - p, 3);
setCount(Math.round(eased * target));
if (p < 1) raf = requestAnimationFrame(frame);
}
raf = requestAnimationFrame(frame);
return () => cancelAnimationFrame(raf);
}, [target]);
return (
<section className={styles.sp} style={vars}>
<div className={styles.avatars} aria-hidden="true">
<span className={styles.avatar} />
<span className={styles.avatar} />
<span className={styles.avatar} />
<span className={`${styles.avatar} ${styles.more}`}>+</span>
</div>
<p className={styles.count}><span className={styles.num}>{count.toLocaleString("ko-KR")}</span>명 참여</p>
<p className={styles.label}>이미 함께한 얼리버드</p>
</section>
);
}04 Social proof strip — Measured area 5.21% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · 9 Promotion Landing Page Templates — Copy-Paste
The post this demo lives in
9 Promotion Landing Page Templates — Copy-PasteA promotion landing page template packages the sections a campaign launch runs on — countdown, proof, pricing, sign-up — See all demos in this post


