09Trust metrics band
Customer count, average score, and first-response time climb a step at a time to their final values, and the certification shapes below stamp themselves in turn.
Best forTrust-reinforcing bands above or below a pricing table
steps(1, end)grid-areaanimation-fill-mode

<main class="stage">
<section class="tb is-demo" aria-label="Trust numbers and badges">
<div class="tb__band">
<div class="tb__metrics">
<div class="tb__cell tb__cell--1">
<p class="tb__label"><svg viewBox="0 0 256 256" width="14" height="14" aria-hidden="true" focusable="false" fill="currentColor"><path d="M168,144a40,40,0,1,1-40-40A40,40,0,0,1,168,144ZM64,56A32,32,0,1,0,96,88,32,32,0,0,0,64,56Zm128,0a32,32,0,1,0,32,32A32,32,0,0,0,192,56Z" opacity="0.2"/><path d="M244.8,150.4a8,8,0,0,1-11.2-1.6A51.6,51.6,0,0,0,192,128a8,8,0,0,1,0-16,24,24,0,1,0-23.24-30,8,8,0,1,1-15.5-4A40,40,0,1,1,219,117.51a67.94,67.94,0,0,1,27.43,21.68A8,8,0,0,1,244.8,150.4ZM190.92,212a8,8,0,1,1-13.85,8,57,57,0,0,0-98.15,0,8,8,0,1,1-13.84-8,72.06,72.06,0,0,1,33.74-29.92,48,48,0,1,1,58.36,0A72.06,72.06,0,0,1,190.92,212ZM128,176a32,32,0,1,0-32-32A32,32,0,0,0,128,176ZM72,120a8,8,0,0,0-8-8A24,24,0,1,1,87.24,82a8,8,0,1,0,15.5-4A40,40,0,1,0,37,117.51,67.94,67.94,0,0,0,9.6,139.19a8,8,0,1,0,12.8,9.61A51.6,51.6,0,0,1,64,128,8,8,0,0,0,72,120Z"/></svg><span class="tb__labelT">Teams onboard</span></p>
<p class="tb__stack">
<span class="tb__n" aria-hidden="true">812</span>
<span class="tb__n" aria-hidden="true">1,140</span>
<span class="tb__n" aria-hidden="true">1,382</span>
<span class="tb__n">1,509</span>
</p>
</div>
<div class="tb__cell tb__cell--2">
<p class="tb__label"><svg viewBox="0 0 256 256" width="14" height="14" aria-hidden="true" focusable="false" fill="currentColor"><path d="M229.06,108.79l-48.7,42,14.88,62.79a8.4,8.4,0,0,1-12.52,9.17L128,189.09,73.28,222.74a8.4,8.4,0,0,1-12.52-9.17l14.88-62.79-48.7-42A8.46,8.46,0,0,1,31.73,94L95.64,88.8l24.62-59.6a8.36,8.36,0,0,1,15.48,0l24.62,59.6L224.27,94A8.46,8.46,0,0,1,229.06,108.79Z" opacity="0.2"/><path d="M239.18,97.26A16.38,16.38,0,0,0,224.92,86l-59-4.76L143.14,26.15a16.36,16.36,0,0,0-30.27,0L90.11,81.23,31.08,86a16.46,16.46,0,0,0-9.37,28.86l45,38.83L53,211.75a16.38,16.38,0,0,0,24.5,17.82L128,198.49l50.53,31.08A16.4,16.4,0,0,0,203,211.75l-13.76-58.07,45-38.83A16.43,16.43,0,0,0,239.18,97.26Zm-15.34,5.47-48.7,42a8,8,0,0,0-2.56,7.91l14.88,62.8a.37.37,0,0,1-.17.48c-.18.14-.23.11-.38,0l-54.72-33.65a8,8,0,0,0-8.38,0L69.09,215.94c-.15.09-.19.12-.38,0a.37.37,0,0,1-.17-.48l14.88-62.8a8,8,0,0,0-2.56-7.91l-48.7-42c-.12-.1-.23-.19-.13-.5s.18-.27.33-.29l63.92-5.16A8,8,0,0,0,103,91.86l24.62-59.61c.08-.17.11-.25.35-.25s.27.08.35.25L153,91.86a8,8,0,0,0,6.75,4.92l63.92,5.16c.15,0,.24,0,.33.29S224,102.63,223.84,102.73Z"/></svg><span class="tb__labelT">Average rating</span></p>
<p class="tb__stack">
<span class="tb__n" aria-hidden="true">3.9</span>
<span class="tb__n" aria-hidden="true">4.2</span>
<span class="tb__n" aria-hidden="true">4.5</span>
<span class="tb__n">4.7</span>
</p>
</div>
<div class="tb__cell tb__cell--3">
<p class="tb__label"><svg viewBox="0 0 256 256" width="14" height="14" aria-hidden="true" focusable="false" fill="currentColor"><path d="M224,128a96,96,0,1,1-96-96A96,96,0,0,1,224,128Z" opacity="0.2"/><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm64-88a8,8,0,0,1-8,8H128a8,8,0,0,1-8-8V72a8,8,0,0,1,16,0v48h48A8,8,0,0,1,192,128Z"/></svg><span class="tb__labelT">First response</span></p>
<p class="tb__stack">
<span class="tb__n" aria-hidden="true">38 min</span>
<span class="tb__n" aria-hidden="true">26 min</span>
<span class="tb__n" aria-hidden="true">17 min</span>
<span class="tb__n">12 min</span>
</p>
</div>
</div>
<div class="tb__foot">
<ul class="tb__badges">
<li class="tb__badge tb__badge--1"><svg viewBox="0 0 256 256" width="14" height="14" aria-hidden="true" focusable="false" fill="currentColor"><path d="M232,128c0,12.51-17.82,21.95-22.68,33.69-4.68,11.32,1.42,30.65-7.78,39.85s-28.53,3.1-39.85,7.78C150,214.18,140.5,232,128,232s-22-17.82-33.69-22.68c-11.32-4.68-30.65,1.42-39.85-7.78s-3.1-28.53-7.78-39.85C41.82,150,24,140.5,24,128s17.82-22,22.68-33.69C51.36,83,45.26,63.66,54.46,54.46S83,51.36,94.31,46.68C106.05,41.82,115.5,24,128,24S150,41.82,161.69,46.68c11.32,4.68,30.65-1.42,39.85,7.78s3.1,28.53,7.78,39.85C214.18,106.05,232,115.5,232,128Z" opacity="0.2"/><path d="M225.86,102.82c-3.77-3.94-7.67-8-9.14-11.57-1.36-3.27-1.44-8.69-1.52-13.94-.15-9.76-.31-20.82-8-28.51s-18.75-7.85-28.51-8c-5.25-.08-10.67-.16-13.94-1.52-3.56-1.47-7.63-5.37-11.57-9.14C146.28,23.51,138.44,16,128,16s-18.27,7.51-25.18,14.14c-3.94,3.77-8,7.67-11.57,9.14C88,40.64,82.56,40.72,77.31,40.8c-9.76.15-20.82.31-28.51,8S41,67.55,40.8,77.31c-.08,5.25-.16,10.67-1.52,13.94-1.47,3.56-5.37,7.63-9.14,11.57C23.51,109.72,16,117.56,16,128s7.51,18.27,14.14,25.18c3.77,3.94,7.67,8,9.14,11.57,1.36,3.27,1.44,8.69,1.52,13.94.15,9.76.31,20.82,8,28.51s18.75,7.85,28.51,8c5.25.08,10.67.16,13.94,1.52,3.56,1.47,7.63,5.37,11.57,9.14C109.72,232.49,117.56,240,128,240s18.27-7.51,25.18-14.14c3.94-3.77,8-7.67,11.57-9.14,3.27-1.36,8.69-1.44,13.94-1.52,9.76-.15,20.82-.31,28.51-8s7.85-18.75,8-28.51c.08-5.25.16-10.67,1.52-13.94,1.47-3.56,5.37-7.63,9.14-11.57C232.49,146.28,240,138.44,240,128S232.49,109.73,225.86,102.82Zm-11.55,39.29c-4.79,5-9.75,10.17-12.38,16.52-2.52,6.1-2.63,13.07-2.73,19.82-.1,7-.21,14.33-3.32,17.43s-10.39,3.22-17.43,3.32c-6.75.1-13.72.21-19.82,2.73-6.35,2.63-11.52,7.59-16.52,12.38S132,224,128,224s-9.15-4.92-14.11-9.69-10.17-9.75-16.52-12.38c-6.1-2.52-13.07-2.63-19.82-2.73-7-.1-14.33-.21-17.43-3.32s-3.22-10.39-3.32-17.43c-.1-6.75-.21-13.72-2.73-19.82-2.63-6.35-7.59-11.52-12.38-16.52S32,132,32,128s4.92-9.15,9.69-14.11,9.75-10.17,12.38-16.52c2.52-6.1,2.63-13.07,2.73-19.82.1-7,.21-14.33,3.32-17.43S70.51,56.9,77.55,56.8c6.75-.1,13.72-.21,19.82-2.73,6.35-2.63,11.52-7.59,16.52-12.38S124,32,128,32s9.15,4.92,14.11,9.69,10.17,9.75,16.52,12.38c6.1,2.52,13.07,2.63,19.82,2.73,7,.1,14.33.21,17.43,3.32s3.22,10.39,3.32,17.43c.1,6.75.21,13.72,2.73,19.82,2.63,6.35,7.59,11.52,12.38,16.52S224,124,224,128,219.08,137.15,214.31,142.11ZM173.66,98.34a8,8,0,0,1,0,11.32l-56,56a8,8,0,0,1-11.32,0l-24-24a8,8,0,0,1,11.32-11.32L112,148.69l50.34-50.35A8,8,0,0,1,173.66,98.34Z"/></svg><span class="tb__badgeT">Security review passed</span></li>
<li class="tb__badge tb__badge--2"><svg viewBox="0 0 256 256" width="14" height="14" aria-hidden="true" focusable="false" fill="currentColor"><path d="M216,56v56c0,96-88,120-88,120S40,208,40,112V56a8,8,0,0,1,8-8H208A8,8,0,0,1,216,56Z" opacity="0.2"/><path d="M208,40H48A16,16,0,0,0,32,56v56c0,52.72,25.52,84.67,46.93,102.19,23.06,18.86,46,25.26,47,25.53a8,8,0,0,0,4.2,0c1-.27,23.91-6.67,47-25.53C198.48,196.67,224,164.72,224,112V56A16,16,0,0,0,208,40Zm0,72c0,37.07-13.66,67.16-40.6,89.42A129.3,129.3,0,0,1,128,223.62a128.25,128.25,0,0,1-38.92-21.81C61.82,179.51,48,149.3,48,112l0-56,160,0ZM82.34,141.66a8,8,0,0,1,11.32-11.32L112,148.69l50.34-50.35a8,8,0,0,1,11.32,11.32l-56,56a8,8,0,0,1-11.32,0Z"/></svg><span class="tb__badgeT">Privacy handling reviewed</span></li>
<li class="tb__badge tb__badge--3"><svg viewBox="0 0 256 256" width="14" height="14" aria-hidden="true" focusable="false" fill="currentColor"><path d="M224,128a96,96,0,1,1-96-96A96,96,0,0,1,224,128Z" opacity="0.2"/><path d="M173.66,98.34a8,8,0,0,1,0,11.32l-56,56a8,8,0,0,1-11.32,0l-24-24a8,8,0,0,1,11.32-11.32L112,148.69l50.34-50.35A8,8,0,0,1,173.66,98.34ZM232,128A104,104,0,1,1,128,24,104.11,104.11,0,0,1,232,128Zm-16,0a88,88,0,1,0-88,88A88.1,88.1,0,0,0,216,128Z"/></svg><span class="tb__badgeT">Accessibility self-check</span></li>
</ul>
<button class="tb__replay" type="button" aria-label="Replay the numbers">
<svg viewBox="0 0 256 256" width="14" height="14" aria-hidden="true" focusable="false" fill="currentColor"><path d="M232,56v64L168,56Z" opacity="0.2"/><path d="M232,48H168a8,8,0,0,0-5.66,13.66L188.69,88,136,140.69l-34.34-34.35a8,8,0,0,0-11.32,0l-72,72a8,8,0,0,0,11.32,11.32L96,123.31l34.34,34.35a8,8,0,0,0,11.32,0L200,99.31l26.34,26.35A8,8,0,0,0,240,120V56A8,8,0,0,0,232,48Zm-8,52.69L187.31,64H224Z"/></svg>
</button>
</div>
</div>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// 276 후기·소셜 프루프 팔레트 — 아홉 항목이 같은 값을 쓴다
$pf-ink: #1c1b22;
$pf-cream: #fbf7ef; // 잉크 위 숫자 (15.99:1)
$pf-teal: #479089;
$pf-violet: #a49cce;
$pf-mint: #b3dad5; // 잉크 위 라벨 (11.45:1)
$pf-amber: $stage-yellow;
$duration: $dur-loop; // 숫자 네 벌이 한 칸씩 갈아 끼워지고 배지 셋이 찍히는 한 바퀴
$easing: $ease-spring; // 배지가 내려앉는 감속
$color: $stage-ink; // 무대
@include stage($stage-ink);
.tb {
box-sizing: border-box;
width: 100%;
max-width: 432px;
}
.tb__band {
box-sizing: border-box;
display: grid;
gap: $sp-4;
padding: $sp-4;
border: 1px solid rgba($pf-cream, .14);
border-radius: $r-card;
background: linear-gradient(135deg, rgba($pf-violet, .2), rgba($pf-teal, .16));
}
// 게이지·겹치는 칸은 grid — 칸 폭이 정확히 균등해진다
.tb__metrics {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: $sp-3;
}
.tb__cell {
display: grid;
gap: $sp-1;
justify-items: center;
min-width: 0;
text-align: center;
}
// p·ul 의 기본 margin 은 0 으로 명시한다
.tb__label {
display: flex;
align-items: center;
gap: $sp-1;
margin: 0;
color: $pf-mint;
font-weight: 600;
font-size: clamp(9px, 2.2vw, 11px);
line-height: 1.2;
white-space: nowrap;
}
.tb__cell--2 .tb__label { color: $pf-amber; }
// 숫자 네 벌을 같은 칸에 포갠다 — 정적 경로에서는 마지막(최종값)만 보인다
.tb__stack {
display: grid;
margin: 0;
}
.tb__n {
grid-area: 1 / 1;
opacity: 0;
color: $pf-cream;
font-weight: 800;
font-size: clamp(17px, 5.2vw, 28px);
line-height: 1.1;
letter-spacing: -.02em;
white-space: nowrap;
}
.tb__n:last-child { opacity: 1; }
.tb__foot {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: $sp-2;
}
.tb__badges {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: $sp-2;
margin: 0;
padding: 0;
list-style: none;
}
// 인증 배지 — 일반 도형(알약) 테두리. 실재 인증기관 이름·로고는 쓰지 않는다
.tb__badge {
display: flex;
align-items: center;
gap: $sp-1;
padding: $sp-1 $sp-2;
border: 2px solid rgba($pf-mint, .45);
border-radius: $r-pill;
background: rgba($pf-mint, .1);
color: $pf-cream;
font-weight: 600;
font-size: clamp(8px, 2vw, 10px);
line-height: 1.2;
white-space: nowrap;
}
.tb__badge svg {
flex: 0 0 auto;
color: $pf-mint;
}
.tb__replay {
flex: 0 0 auto;
display: grid;
place-items: center;
width: 24px;
height: 24px;
padding: 0;
border: 1px solid rgba($pf-cream, .28);
border-radius: 50%;
background: rgba($pf-ink, .55);
color: $pf-mint;
cursor: pointer;
}
.tb__replay:focus-visible {
outline: 2px solid $pf-mint;
outline-offset: 2px;
}
// ── 자동 재생 ─────────────────────────────────────────────
// 카운트업은 크로스페이드가 아니라 steps(1, end) 한 칸 컷이다 (반투명 유령이 안 생긴다).
// 컷만으로는 프레임이 모자라므로 그 아래에서 배지가 scale·translateY 로 차례로 찍힌다.
// animation 단축은 delay·fill-mode 를 리셋하므로 낱개 속성으로 쓴다
.tb.is-demo .tb__n,
.tb.is-count .tb__n {
animation-duration: $duration;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.tb.is-demo .tb__n:nth-child(1), .tb.is-count .tb__n:nth-child(1) { animation-name: tbSlot1; }
.tb.is-demo .tb__n:nth-child(2), .tb.is-count .tb__n:nth-child(2) { animation-name: tbSlot2; }
.tb.is-demo .tb__n:nth-child(3), .tb.is-count .tb__n:nth-child(3) { animation-name: tbSlot3; }
.tb.is-demo .tb__n:nth-child(4), .tb.is-count .tb__n:nth-child(4) { animation-name: tbSlot4; }
// 세 칸의 스텝 시점을 살짝 어긋나게 해서 프레임을 고루 채운다
.tb.is-demo .tb__cell--2 .tb__n, .tb.is-count .tb__cell--2 .tb__n { animation-delay: -.09s; }
.tb.is-demo .tb__cell--3 .tb__n, .tb.is-count .tb__cell--3 .tb__n { animation-delay: -.18s; }
.tb.is-demo .tb__badge,
.tb.is-count .tb__badge {
animation-name: tbStamp;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.tb.is-demo .tb__badge--2, .tb.is-count .tb__badge--2 { animation-delay: .18s; }
.tb.is-demo .tb__badge--3, .tb.is-count .tb__badge--3 { animation-delay: .36s; }
// 다시 세기 — 같은 키프레임을 한 번만 돌리고 최종 상태에서 멈춘다
.tb.is-count .tb__n,
.tb.is-count .tb__badge {
animation-iteration-count: 1;
animation-fill-mode: both;
}
@keyframes tbSlot1 { 0% { opacity: 1; } 22%, 100% { opacity: 0; } }
@keyframes tbSlot2 { 0% { opacity: 0; } 22% { opacity: 1; } 44%, 100% { opacity: 0; } }
@keyframes tbSlot3 { 0% { opacity: 0; } 44% { opacity: 1; } 66%, 100% { opacity: 0; } }
// 마지막 값은 66% 에 도착해 끝까지 유지한다 — 24프레임의 마지막 장은 95.8% 지점이다
@keyframes tbSlot4 { 0% { opacity: 0; } 66%, 100% { opacity: 1; } }
// 도장처럼 내려찍힌다 — 마지막 배지도 지연 .36s 를 더해 40% 안에 앉는다
@keyframes tbStamp {
0% { transform: translateY(-8px) scale(.6); opacity: 0; }
5% { transform: translateY(-4px) scale(.82); opacity: 1; }
14% { transform: translateY(0) scale(1.08); opacity: 1; }
22%, 100% { transform: translateY(0) scale(1); opacity: 1; }
}
// 좁은 화면 — 세 칸을 유지하고 여백·아이콘만 줄인다
@media (max-width: 340px), (max-height: 220px) {
.tb__band { gap: $sp-2; padding: $sp-2; }
.tb__metrics { gap: $sp-1; }
.tb__foot { gap: $sp-1; }
.tb__badges { gap: $sp-1; }
.tb__badge { padding: 0 $sp-1; }
.tb__label svg, .tb__badge svg { width: 12px; height: 12px; }
.tb__replay { width: 20px; height: 20px; }
}
@media (prefers-reduced-motion: reduce) {
// .tb.is-demo .tb__n (0,3,0) 를 이기려면 !important 가 필요하다
.tb__n, .tb__badge { animation: none !important; }
}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);
}
.tb {
box-sizing: border-box;
width: 100%;
max-width: 432px;
}
.tb__band {
box-sizing: border-box;
display: grid;
gap: 16px;
padding: 16px;
border: 1px solid rgba(251, 247, 239, 0.14);
border-radius: 16px;
background: linear-gradient(135deg, rgba(164, 156, 206, 0.2), rgba(71, 144, 137, 0.16));
}
.tb__metrics {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.tb__cell {
display: grid;
gap: 4px;
justify-items: center;
min-width: 0;
text-align: center;
}
.tb__label {
display: flex;
align-items: center;
gap: 4px;
margin: 0;
color: #b3dad5;
font-weight: 600;
font-size: clamp(9px, 2.2vw, 11px);
line-height: 1.2;
white-space: nowrap;
}
.tb__cell--2 .tb__label {
color: #ffd23f;
}
.tb__stack {
display: grid;
margin: 0;
}
.tb__n {
grid-area: 1/1;
opacity: 0;
color: #fbf7ef;
font-weight: 800;
font-size: clamp(17px, 5.2vw, 28px);
line-height: 1.1;
letter-spacing: -0.02em;
white-space: nowrap;
}
.tb__n:last-child {
opacity: 1;
}
.tb__foot {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 8px;
}
.tb__badges {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 8px;
margin: 0;
padding: 0;
list-style: none;
}
.tb__badge {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
border: 2px solid rgba(179, 218, 213, 0.45);
border-radius: 999px;
background: rgba(179, 218, 213, 0.1);
color: #fbf7ef;
font-weight: 600;
font-size: clamp(8px, 2vw, 10px);
line-height: 1.2;
white-space: nowrap;
}
.tb__badge svg {
flex: 0 0 auto;
color: #b3dad5;
}
.tb__replay {
flex: 0 0 auto;
display: grid;
place-items: center;
width: 24px;
height: 24px;
padding: 0;
border: 1px solid rgba(251, 247, 239, 0.28);
border-radius: 50%;
background: rgba(28, 27, 34, 0.55);
color: #b3dad5;
cursor: pointer;
}
.tb__replay:focus-visible {
outline: 2px solid #b3dad5;
outline-offset: 2px;
}
.tb.is-demo .tb__n,
.tb.is-count .tb__n {
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.tb.is-demo .tb__n:nth-child(1), .tb.is-count .tb__n:nth-child(1) {
animation-name: tbSlot1;
}
.tb.is-demo .tb__n:nth-child(2), .tb.is-count .tb__n:nth-child(2) {
animation-name: tbSlot2;
}
.tb.is-demo .tb__n:nth-child(3), .tb.is-count .tb__n:nth-child(3) {
animation-name: tbSlot3;
}
.tb.is-demo .tb__n:nth-child(4), .tb.is-count .tb__n:nth-child(4) {
animation-name: tbSlot4;
}
.tb.is-demo .tb__cell--2 .tb__n, .tb.is-count .tb__cell--2 .tb__n {
animation-delay: -0.09s;
}
.tb.is-demo .tb__cell--3 .tb__n, .tb.is-count .tb__cell--3 .tb__n {
animation-delay: -0.18s;
}
.tb.is-demo .tb__badge,
.tb.is-count .tb__badge {
animation-name: tbStamp;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.tb.is-demo .tb__badge--2, .tb.is-count .tb__badge--2 {
animation-delay: 0.18s;
}
.tb.is-demo .tb__badge--3, .tb.is-count .tb__badge--3 {
animation-delay: 0.36s;
}
.tb.is-count .tb__n,
.tb.is-count .tb__badge {
animation-iteration-count: 1;
animation-fill-mode: both;
}
@keyframes tbSlot1 {
0% {
opacity: 1;
}
22%, 100% {
opacity: 0;
}
}
@keyframes tbSlot2 {
0% {
opacity: 0;
}
22% {
opacity: 1;
}
44%, 100% {
opacity: 0;
}
}
@keyframes tbSlot3 {
0% {
opacity: 0;
}
44% {
opacity: 1;
}
66%, 100% {
opacity: 0;
}
}
@keyframes tbSlot4 {
0% {
opacity: 0;
}
66%, 100% {
opacity: 1;
}
}
@keyframes tbStamp {
0% {
transform: translateY(-8px) scale(0.6);
opacity: 0;
}
5% {
transform: translateY(-4px) scale(0.82);
opacity: 1;
}
14% {
transform: translateY(0) scale(1.08);
opacity: 1;
}
22%, 100% {
transform: translateY(0) scale(1);
opacity: 1;
}
}
@media (max-width: 340px), (max-height: 220px) {
.tb__band {
gap: 8px;
padding: 8px;
}
.tb__metrics {
gap: 4px;
}
.tb__foot {
gap: 4px;
}
.tb__badges {
gap: 4px;
}
.tb__badge {
padding: 0 4px;
}
.tb__label svg, .tb__badge svg {
width: 12px;
height: 12px;
}
.tb__replay {
width: 20px;
height: 20px;
}
}
@media (prefers-reduced-motion: reduce) {
.tb__n, .tb__badge {
animation: none !important;
}
}(function () {
var root = document.querySelector('.tb');
var replay = root.querySelector('.tb__replay');
function stopLoop() { root.classList.remove('is-demo'); }
// 다시 세기 — 루프를 끄고 카운트업·도장을 한 번만 돌린다 (끝나면 최종값에서 멈춘다)
replay.addEventListener('click', function () {
stopLoop();
root.classList.remove('is-count');
void root.offsetWidth; // 클래스를 다시 붙여도 애니메이션이 되감기도록 리플로를 한 번
root.classList.add('is-count');
});
root.addEventListener('pointerenter', stopLoop);
root.addEventListener('touchstart', stopLoop, { passive: true });
root.addEventListener('focusin', stopLoop);
})();import { useRef, useState } from "react";
import type { CSSProperties, ReactNode } from "react";
import styles from "./TrustMetricsBand.module.scss";
type Box<T> = { current: T };
export type Metric = {
/** 아이콘과 함께 읽는 라벨 (예 "쓰는 팀") */
label: string;
/** 카운트업 값 — 마지막 값이 최종값 */
values: string[];
/** 별점 칸은 라벨을 앰버로 칠한다 */
tone?: "mint" | "amber";
};
export type Badge = {
text: string;
icon: ReactNode;
};
export type TrustMetricsBandProps = {
label?: string;
replayLabel?: string;
metrics?: Metric[];
badges?: Badge[];
/** 숫자 네 벌이 갈아 끼워지고 배지 셋이 찍히는 한 바퀴(ms) */
duration?: number;
easing?: string;
/** 무대 색 */
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const UsersIcon = (): ReactNode => (
<svg viewBox="0 0 256 256" width="14" height="14" aria-hidden="true" focusable="false" fill="currentColor">
<path d="M168,144a40,40,0,1,1-40-40A40,40,0,0,1,168,144ZM64,56A32,32,0,1,0,96,88,32,32,0,0,0,64,56Zm128,0a32,32,0,1,0,32,32A32,32,0,0,0,192,56Z" opacity="0.2" />
<path d="M244.8,150.4a8,8,0,0,1-11.2-1.6A51.6,51.6,0,0,0,192,128a8,8,0,0,1,0-16,24,24,0,1,0-23.24-30,8,8,0,1,1-15.5-4A40,40,0,1,1,219,117.51a67.94,67.94,0,0,1,27.43,21.68A8,8,0,0,1,244.8,150.4ZM190.92,212a8,8,0,1,1-13.85,8,57,57,0,0,0-98.15,0,8,8,0,1,1-13.84-8,72.06,72.06,0,0,1,33.74-29.92,48,48,0,1,1,58.36,0A72.06,72.06,0,0,1,190.92,212ZM128,176a32,32,0,1,0-32-32A32,32,0,0,0,128,176ZM72,120a8,8,0,0,0-8-8A24,24,0,1,1,87.24,82a8,8,0,1,0,15.5-4A40,40,0,1,0,37,117.51,67.94,67.94,0,0,0,9.6,139.19a8,8,0,1,0,12.8,9.61A51.6,51.6,0,0,1,64,128,8,8,0,0,0,72,120Z" />
</svg>
);
const StarIcon = (): ReactNode => (
<svg viewBox="0 0 256 256" width="14" height="14" aria-hidden="true" focusable="false" fill="currentColor">
<path d="M229.06,108.79l-48.7,42,14.88,62.79a8.4,8.4,0,0,1-12.52,9.17L128,189.09,73.28,222.74a8.4,8.4,0,0,1-12.52-9.17l14.88-62.79-48.7-42A8.46,8.46,0,0,1,31.73,94L95.64,88.8l24.62-59.6a8.36,8.36,0,0,1,15.48,0l24.62,59.6L224.27,94A8.46,8.46,0,0,1,229.06,108.79Z" opacity="0.2" />
<path d="M239.18,97.26A16.38,16.38,0,0,0,224.92,86l-59-4.76L143.14,26.15a16.36,16.36,0,0,0-30.27,0L90.11,81.23,31.08,86a16.46,16.46,0,0,0-9.37,28.86l45,38.83L53,211.75a16.38,16.38,0,0,0,24.5,17.82L128,198.49l50.53,31.08A16.4,16.4,0,0,0,203,211.75l-13.76-58.07,45-38.83A16.43,16.43,0,0,0,239.18,97.26Zm-15.34,5.47-48.7,42a8,8,0,0,0-2.56,7.91l14.88,62.8a.37.37,0,0,1-.17.48c-.18.14-.23.11-.38,0l-54.72-33.65a8,8,0,0,0-8.38,0L69.09,215.94c-.15.09-.19.12-.38,0a.37.37,0,0,1-.17-.48l14.88-62.8a8,8,0,0,0-2.56-7.91l-48.7-42c-.12-.1-.23-.19-.13-.5s.18-.27.33-.29l63.92-5.16A8,8,0,0,0,103,91.86l24.62-59.61c.08-.17.11-.25.35-.25s.27.08.35.25L153,91.86a8,8,0,0,0,6.75,4.92l63.92,5.16c.15,0,.24,0,.33.29S224,102.63,223.84,102.73Z" />
</svg>
);
const ClockIcon = (): ReactNode => (
<svg viewBox="0 0 256 256" width="14" height="14" aria-hidden="true" focusable="false" fill="currentColor">
<path d="M224,128a96,96,0,1,1-96-96A96,96,0,0,1,224,128Z" opacity="0.2" />
<path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm64-88a8,8,0,0,1-8,8H128a8,8,0,0,1-8-8V72a8,8,0,0,1,16,0v48h48A8,8,0,0,1,192,128Z" />
</svg>
);
const SealIcon = (): ReactNode => (
<svg viewBox="0 0 256 256" width="14" height="14" aria-hidden="true" focusable="false" fill="currentColor">
<path d="M232,128c0,12.51-17.82,21.95-22.68,33.69-4.68,11.32,1.42,30.65-7.78,39.85s-28.53,3.1-39.85,7.78C150,214.18,140.5,232,128,232s-22-17.82-33.69-22.68c-11.32-4.68-30.65,1.42-39.85-7.78s-3.1-28.53-7.78-39.85C41.82,150,24,140.5,24,128s17.82-22,22.68-33.69C51.36,83,45.26,63.66,54.46,54.46S83,51.36,94.31,46.68C106.05,41.82,115.5,24,128,24S150,41.82,161.69,46.68c11.32,4.68,30.65-1.42,39.85,7.78s3.1,28.53,7.78,39.85C214.18,106.05,232,115.5,232,128Z" opacity="0.2" />
<path d="M225.86,102.82c-3.77-3.94-7.67-8-9.14-11.57-1.36-3.27-1.44-8.69-1.52-13.94-.15-9.76-.31-20.82-8-28.51s-18.75-7.85-28.51-8c-5.25-.08-10.67-.16-13.94-1.52-3.56-1.47-7.63-5.37-11.57-9.14C146.28,23.51,138.44,16,128,16s-18.27,7.51-25.18,14.14c-3.94,3.77-8,7.67-11.57,9.14C88,40.64,82.56,40.72,77.31,40.8c-9.76.15-20.82.31-28.51,8S41,67.55,40.8,77.31c-.08,5.25-.16,10.67-1.52,13.94-1.47,3.56-5.37,7.63-9.14,11.57C23.51,109.72,16,117.56,16,128s7.51,18.27,14.14,25.18c3.77,3.94,7.67,8,9.14,11.57,1.36,3.27,1.44,8.69,1.52,13.94.15,9.76.31,20.82,8,28.51s18.75,7.85,28.51,8c5.25.08,10.67.16,13.94,1.52,3.56,1.47,7.63,5.37,11.57,9.14C109.72,232.49,117.56,240,128,240s18.27-7.51,25.18-14.14c3.94-3.77,8-7.67,11.57-9.14,3.27-1.36,8.69-1.44,13.94-1.52,9.76-.15,20.82-.31,28.51-8s7.85-18.75,8-28.51c.08-5.25.16-10.67,1.52-13.94,1.47-3.56,5.37-7.63,9.14-11.57C232.49,146.28,240,138.44,240,128S232.49,109.73,225.86,102.82Zm-11.55,39.29c-4.79,5-9.75,10.17-12.38,16.52-2.52,6.1-2.63,13.07-2.73,19.82-.1,7-.21,14.33-3.32,17.43s-10.39,3.22-17.43,3.32c-6.75.1-13.72.21-19.82,2.73-6.35,2.63-11.52,7.59-16.52,12.38S132,224,128,224s-9.15-4.92-14.11-9.69-10.17-9.75-16.52-12.38c-6.1-2.52-13.07-2.63-19.82-2.73-7-.1-14.33-.21-17.43-3.32s-3.22-10.39-3.32-17.43c-.1-6.75-.21-13.72-2.73-19.82-2.63-6.35-7.59-11.52-12.38-16.52S32,132,32,128s4.92-9.15,9.69-14.11,9.75-10.17,12.38-16.52c2.52-6.1,2.63-13.07,2.73-19.82.1-7,.21-14.33,3.32-17.43S70.51,56.9,77.55,56.8c6.75-.1,13.72-.21,19.82-2.73,6.35-2.63,11.52-7.59,16.52-12.38S124,32,128,32s9.15,4.92,14.11,9.69,10.17,9.75,16.52,12.38c6.1,2.52,13.07,2.63,19.82,2.73,7,.1,14.33.21,17.43,3.32s3.22,10.39,3.32,17.43c.1,6.75.21,13.72,2.73,19.82,2.63,6.35,7.59,11.52,12.38,16.52S224,124,224,128,219.08,137.15,214.31,142.11ZM173.66,98.34a8,8,0,0,1,0,11.32l-56,56a8,8,0,0,1-11.32,0l-24-24a8,8,0,0,1,11.32-11.32L112,148.69l50.34-50.35A8,8,0,0,1,173.66,98.34Z" />
</svg>
);
const ShieldIcon = (): ReactNode => (
<svg viewBox="0 0 256 256" width="14" height="14" aria-hidden="true" focusable="false" fill="currentColor">
<path d="M216,56v56c0,96-88,120-88,120S40,208,40,112V56a8,8,0,0,1,8-8H208A8,8,0,0,1,216,56Z" opacity="0.2" />
<path d="M208,40H48A16,16,0,0,0,32,56v56c0,52.72,25.52,84.67,46.93,102.19,23.06,18.86,46,25.26,47,25.53a8,8,0,0,0,4.2,0c1-.27,23.91-6.67,47-25.53C198.48,196.67,224,164.72,224,112V56A16,16,0,0,0,208,40Zm0,72c0,37.07-13.66,67.16-40.6,89.42A129.3,129.3,0,0,1,128,223.62a128.25,128.25,0,0,1-38.92-21.81C61.82,179.51,48,149.3,48,112l0-56,160,0ZM82.34,141.66a8,8,0,0,1,11.32-11.32L112,148.69l50.34-50.35a8,8,0,0,1,11.32,11.32l-56,56a8,8,0,0,1-11.32,0Z" />
</svg>
);
const CheckIcon = (): ReactNode => (
<svg viewBox="0 0 256 256" width="14" height="14" aria-hidden="true" focusable="false" fill="currentColor">
<path d="M224,128a96,96,0,1,1-96-96A96,96,0,0,1,224,128Z" opacity="0.2" />
<path d="M173.66,98.34a8,8,0,0,1,0,11.32l-56,56a8,8,0,0,1-11.32,0l-24-24a8,8,0,0,1,11.32-11.32L112,148.69l50.34-50.35A8,8,0,0,1,173.66,98.34ZM232,128A104,104,0,1,1,128,24,104.11,104.11,0,0,1,232,128Zm-16,0a88,88,0,1,0-88,88A88.1,88.1,0,0,0,216,128Z" />
</svg>
);
const TrendIcon = (): ReactNode => (
<svg viewBox="0 0 256 256" width="14" height="14" aria-hidden="true" focusable="false" fill="currentColor">
<path d="M232,56v64L168,56Z" opacity="0.2" />
<path d="M232,48H168a8,8,0,0,0-5.66,13.66L188.69,88,136,140.69l-34.34-34.35a8,8,0,0,0-11.32,0l-72,72a8,8,0,0,0,11.32,11.32L96,123.31l34.34,34.35a8,8,0,0,0,11.32,0L200,99.31l26.34,26.35A8,8,0,0,0,240,120V56A8,8,0,0,0,232,48Zm-8,52.69L187.31,64H224Z" />
</svg>
);
const DEFAULT_METRICS: Metric[] = [
{ label: "쓰는 팀", values: ["812곳", "1,140곳", "1,382곳", "1,509곳"], tone: "mint" },
{ label: "평균 평점", values: ["3.9", "4.2", "4.5", "4.7"], tone: "amber" },
{ label: "첫 응답", values: ["38분", "26분", "17분", "12분"], tone: "mint" },
];
const DEFAULT_BADGES: Badge[] = [
{ text: "보안 점검 통과", icon: <SealIcon /> },
{ text: "개인정보 처리 점검", icon: <ShieldIcon /> },
{ text: "접근성 자가 점검", icon: <CheckIcon /> },
];
const METRIC_ICONS = [<UsersIcon />, <StarIcon />, <ClockIcon />];
const SLOTS = [styles.slot1, styles.slot2, styles.slot3, styles.slot4];
const SKEWS = [styles.skew0, styles.skew1, styles.skew2];
const BADGE_TONES = [styles.badgeA, styles.badgeB, styles.badgeC];
export default function TrustMetricsBand({
label = "신뢰 수치와 인증 배지",
replayLabel = "수치 다시 세기",
metrics = DEFAULT_METRICS,
badges = DEFAULT_BADGES,
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#17141a",
}: TrustMetricsBandProps): ReactNode {
const [loop, setLoop] = useState<boolean>(true);
const [count, setCount] = useState<boolean>(false);
/* 다시 세기 — 같은 애니메이션을 되감기 위해 키를 바꿔 트리를 새로 얹는다 */
const [run, setRun] = useState<number>(0);
const seq = useRef<number>(0) as Box<number>;
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
const replay = (): void => {
setLoop(false);
setCount(false);
seq.current += 1;
setRun(seq.current);
window.setTimeout(() => setCount(true), 0);
};
const rootClass = [styles.tb, loop ? styles.isDemo : "", count ? styles.isCount : ""].join(" ").trim();
return (
<section
className={rootClass}
style={vars}
aria-label={label}
key={run}
onPointerEnter={() => setLoop(false)}
onFocus={() => setLoop(false)}
>
<div className={styles.band}>
<div className={styles.metrics}>
{metrics.map((m: Metric, i: number) => (
<div className={styles.cell} key={m.label}>
<p className={`${styles.label} ${m.tone === "amber" ? styles.cellAmber : ""}`}>
{METRIC_ICONS[i % METRIC_ICONS.length]}
<span>{m.label}</span>
</p>
<p className={styles.stack}>
{m.values.map((v: string, j: number) => (
<span
className={[styles.n, SLOTS[j % SLOTS.length], SKEWS[i % SKEWS.length], j === m.values.length - 1 ? styles.last : ""].join(" ").trim()}
aria-hidden={j !== m.values.length - 1 ? true : undefined}
key={v}
>
{v}
</span>
))}
</p>
</div>
))}
</div>
<div className={styles.foot}>
<ul className={styles.badges}>
{badges.map((b: Badge, i: number) => (
<li className={`${styles.badge} ${BADGE_TONES[i % BADGE_TONES.length]}`} key={b.text}>
{b.icon}
<span>{b.text}</span>
</li>
))}
</ul>
<button className={styles.replay} type="button" aria-label={replayLabel} onClick={replay}>
<TrendIcon />
</button>
</div>
</div>
</section>
);
}09 Trust metrics band — Measured area 7.978% · moving frames 16/23Original by GODRICH · SCSS and React ship in the zip · Testimonial Section Design: 9 Social Proof Blocks
The post this demo lives in
Testimonial Section Design: 9 Social Proof BlocksTestimonial section design is the band of a page that shows proof from real customers — quotes, star ratings, client mar See all demos in this post


