04성공 배지 바운스 체크
원형 배지가 scale(.4→1.12→1)로 튀어 오르는 동안, 그 안 체크 아이콘은 SVG polyline 의 stroke-dashoffset 이 24에서 0으로 줄며 선이 그어지듯 완성됩니다.
쓰임저장 완료 배지, 결제 성공 확인 화면
scale 바운스stroke-dashoffsetopacity

<main class="stage">
<div class="fv4 is-demo">
<span class="fv4__badge">
<svg class="fv4__check" viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
<polyline class="fv4__path" points="4,13 9,18 20,6" />
</svg>
</span>
<span class="fv4__text">저장됐어요</span>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 성공 배지가 scale 로 바운스하며 나타나고, 그 안 체크는 stroke-dashoffset 로 그려진다
$duration: $dur-enter;
$easing: $ease-pop;
$color: $subject-mint;
@include stage($stage-paper);
.fv4 { display: flex; align-items: center; gap: $sp-3; width: min(220px, 86vw); }
.fv4__badge {
position: relative; /* ::after 서지 링의 기준 — 위치·크기 불변 */
width: 40px; height: 40px; border-radius: 50%;
background: $color; display: grid; place-items: center; color: $subject-ink;
opacity: 0; transform: scale(.4);
}
.fv4__path { stroke-dasharray: 24; stroke-dashoffset: 24; }
// 한 겹(감쇠 진동): 배지 등장 순간 rotate 감쇠 진동은 fv4-badge transform 안에 합성(scale 뒤에 rotate 를 얹는다)
.fv4__badge::after {
content: ""; position: absolute; inset: -6px; border-radius: 50%;
box-shadow: 0 0 0 4px rgba($color, .5); opacity: 0; transform: scale(.8); pointer-events: none;
}
.fv4__text { font-weight: 600; font-size: 14px; line-height: 1; font-family: inherit; color: $subject-blue; opacity: 0; transform: translateY(4px); }
@keyframes fv4-badge {
0%, 10% { opacity: 0; transform: scale(.4) rotate(0deg); }
20% { transform: scale(.86) rotate(8deg); }
27% { transform: scale(1.02) rotate(-5deg); }
33% { transform: scale(1.1) rotate(3deg); }
39% { transform: scale(1.04) rotate(-1deg); }
43% { transform: scale(1.06) rotate(0deg); }
48% { transform: scale(1); }
85%, 100% { opacity: 1; transform: scale(1); }
}
@keyframes fv4-surge {
0%, 30% { opacity: 0; transform: scale(.8); }
38% { opacity: .55; transform: scale(1.18); }
50% { opacity: .12; transform: scale(1.32); }
60% { opacity: .22; transform: scale(1.42); }
72% { opacity: 0; transform: scale(1.48); }
100% { opacity: 0; transform: scale(1.48); }
}
@keyframes fv4-draw { 0%, 30% { stroke-dashoffset: 24; } 55%, 100% { stroke-dashoffset: 0; } }
@keyframes fv4-text { 0%, 30% { opacity: 0; transform: translateY(4px); } 55%, 100% { opacity: 1; transform: translateY(0); } }
.fv4.is-demo .fv4__badge { animation: fv4-badge $duration $easing infinite; }
.fv4.is-demo .fv4__badge::after { animation: fv4-surge $duration $easing infinite; }
.fv4.is-demo .fv4__path { animation: fv4-draw $duration linear infinite; }
.fv4.is-demo .fv4__text { animation: fv4-text $duration $easing infinite; }
@media (prefers-reduced-motion: reduce) {
.fv4.is-demo .fv4__badge,
.fv4.is-demo .fv4__badge::after,
.fv4.is-demo .fv4__path,
.fv4.is-demo .fv4__text { animation: none !important; }
.fv4.is-demo .fv4__badge { opacity: 1; transform: scale(1); }
.fv4.is-demo .fv4__badge::after { opacity: 0; }
.fv4.is-demo .fv4__path { stroke-dashoffset: 0; }
.fv4.is-demo .fv4__text { opacity: 1; transform: translateY(0); }
}@charset "UTF-8";
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);
}
.fv4 {
display: flex;
align-items: center;
gap: 12px;
width: min(220px, 86vw);
}
.fv4__badge {
position: relative; /* ::after 서지 링의 기준 — 위치·크기 불변 */
width: 40px;
height: 40px;
border-radius: 50%;
background: #4cd4a6;
display: grid;
place-items: center;
color: #17141a;
opacity: 0;
transform: scale(0.4);
}
.fv4__path {
stroke-dasharray: 24;
stroke-dashoffset: 24;
}
.fv4__badge::after {
content: "";
position: absolute;
inset: -6px;
border-radius: 50%;
box-shadow: 0 0 0 4px rgba(76, 212, 166, 0.5);
opacity: 0;
transform: scale(0.8);
pointer-events: none;
}
.fv4__text {
font-weight: 600;
font-size: 14px;
line-height: 1;
font-family: inherit;
color: #2f6df6;
opacity: 0;
transform: translateY(4px);
}
@keyframes fv4-badge {
0%, 10% {
opacity: 0;
transform: scale(0.4) rotate(0deg);
}
20% {
transform: scale(0.86) rotate(8deg);
}
27% {
transform: scale(1.02) rotate(-5deg);
}
33% {
transform: scale(1.1) rotate(3deg);
}
39% {
transform: scale(1.04) rotate(-1deg);
}
43% {
transform: scale(1.06) rotate(0deg);
}
48% {
transform: scale(1);
}
85%, 100% {
opacity: 1;
transform: scale(1);
}
}
@keyframes fv4-surge {
0%, 30% {
opacity: 0;
transform: scale(0.8);
}
38% {
opacity: 0.55;
transform: scale(1.18);
}
50% {
opacity: 0.12;
transform: scale(1.32);
}
60% {
opacity: 0.22;
transform: scale(1.42);
}
72% {
opacity: 0;
transform: scale(1.48);
}
100% {
opacity: 0;
transform: scale(1.48);
}
}
@keyframes fv4-draw {
0%, 30% {
stroke-dashoffset: 24;
}
55%, 100% {
stroke-dashoffset: 0;
}
}
@keyframes fv4-text {
0%, 30% {
opacity: 0;
transform: translateY(4px);
}
55%, 100% {
opacity: 1;
transform: translateY(0);
}
}
.fv4.is-demo .fv4__badge {
animation: fv4-badge 600ms cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
.fv4.is-demo .fv4__badge::after {
animation: fv4-surge 600ms cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
.fv4.is-demo .fv4__path {
animation: fv4-draw 600ms linear infinite;
}
.fv4.is-demo .fv4__text {
animation: fv4-text 600ms cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
@media (prefers-reduced-motion: reduce) {
.fv4.is-demo .fv4__badge,
.fv4.is-demo .fv4__badge::after,
.fv4.is-demo .fv4__path,
.fv4.is-demo .fv4__text {
animation: none !important;
}
.fv4.is-demo .fv4__badge {
opacity: 1;
transform: scale(1);
}
.fv4.is-demo .fv4__badge::after {
opacity: 0;
}
.fv4.is-demo .fv4__path {
stroke-dashoffset: 0;
}
.fv4.is-demo .fv4__text {
opacity: 1;
transform: translateY(0);
}
}import type { CSSProperties } from "react";
import styles from "./SuccessBounceCheck.module.scss";
export type SuccessBounceCheckProps = { duration?: number; easing?: string; color?: string };
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function SuccessBounceCheck({ duration = 600, easing = "cubic-bezier(.2,1.4,.4,1)", color = "#4cd4a6" }: SuccessBounceCheckProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.fv4} style={vars}>
<span className={styles.badge}>
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" strokeWidth={3} strokeLinecap="round" strokeLinejoin="round">
<polyline className={styles.path} points="4,13 9,18 20,6" />
</svg>
</span>
<span className={styles.text}>저장됐어요</span>
</div>
);
}04 성공 배지 바운스 체크 — 측정 누적면적 3.188% · 움직인 프레임 17/23GODRICH 원작 · zip 에 SCSS·React 포함 · CSS 유효성 검사 애니메이션 9종 — 폼 에러·성공 표시
이 데모가 속한 글
CSS 유효성 검사 애니메이션 9종 — 폼 에러·성공 표시CSS 유효성 검사 애니메이션은 폼에 입력한 값이 맞는지 틀린지를 흔들림이 아니라 색·아이콘·글로우 같은 다른 신호로 알려주는 움직임입니다. 아래 아홉 개는 회원가입 폼에서 실제로 일어나는 순서대로 골라 복붙 한 번 이 글의 데모 전부 보기

