02캡차 확인 체크박스
체크박스를 누르면 로딩 스피너가 잠깐 돌다가 초록 체크로 바뀌면서 "확인됨" 배지가 스프링처럼 튀어 오릅니다.
쓰임인증번호 발송 전 사람인지 확인하는 단계, 무료 체험 신청 폼의 스팸 방지
@keyframes spin → scalerotate(560deg) 스피너:checked 상태

<main class="stage">
<div class="captcha-card is-demo">
<label class="captcha">
<input class="captcha__input" type="checkbox">
<span class="captcha__box">
<span class="captcha__spinner" aria-hidden="true"></span>
<svg class="captcha__check" viewBox="0 0 256 256" aria-hidden="true">
<g fill="currentColor">
<path d="M224 128a96 96 0 1 1-96-96a96 96 0 0 1 96 96" opacity=".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 11.32 0M232 128A104 104 0 1 1 128 24a104.11 104.11 0 0 1 104 104m-16 0a88 88 0 1 0-88 88a88.1 88.1 0 0 0 88-88"/>
</g>
</svg>
</span>
<span class="captcha__label">I'm not a robot</span>
</label>
<span class="captcha__badge">Verified</span>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 스피너 → 체크 전환 주기
$easing: $ease-pop;
$color: $subject-mint; // 확인 색 (상큼한 밝은 축)
@include stage($stage-paper);
.captcha-card {
display: flex;
flex-direction: column;
align-items: center;
gap: $sp-3;
}
.captcha {
display: flex;
align-items: center;
gap: $sp-3;
padding: $sp-3 $sp-5;
background: $stage-paper;
border: 1.5px solid #d8d2c4;
border-radius: $r-card;
box-shadow: $shadow-press;
cursor: pointer;
font-family: inherit;
}
.captcha__input { position: absolute; width: 1px; height: 1px; opacity: 0; }
.captcha__box {
position: relative;
width: 26px; height: 26px;
flex: 0 0 auto;
border-radius: $r-xs;
border: 2px solid #c9c2b2;
background: $stage-paper;
}
.captcha__spinner {
position: absolute;
inset: 3px;
border-radius: 50%;
border: 3px solid rgba($color, .2);
border-top-color: $color;
opacity: 0;
}
.captcha__check {
position: absolute;
inset: 0;
color: $color;
opacity: 0;
transform: scale(.4);
}
.captcha__label { font-size: 13px; font-weight: 600; color: $stage-ink; }
.captcha__badge {
font-size: 11px;
font-weight: 700;
color: $color;
opacity: 0;
transform: translateY(4px);
}
// 실제 사용 — 클릭해서 체크하면 즉시 확인 표시 (is-demo 없이 열었을 때)
.captcha__input:checked ~ .captcha__box .captcha__check { opacity: 1; transform: scale(1); transition: opacity $dur-quick $ease-out, transform $dur-quick $ease-pop; }
.captcha__input:checked ~ .captcha__box { border-color: $color; }
.captcha__input:focus-visible ~ .captcha__box { outline: 2px solid $color; outline-offset: 2px; }
// 자동 재생 루프 — 로딩 → 확인 → 배지 → 리셋
.captcha-card.is-demo .captcha { animation: capt-card $duration $easing infinite; }
.captcha-card.is-demo .captcha__spinner { animation: capt-spinner $duration linear infinite; }
.captcha-card.is-demo .captcha__check { animation: capt-check $duration $easing infinite; }
.captcha-card.is-demo .captcha__badge { animation: capt-badge $duration $ease-out infinite; }
@keyframes capt-card {
0%, 28% { background: $stage-paper; border-color: #d8d2c4; box-shadow: $shadow-press; }
40%, 84% { background: mix($color, white, 12%); border-color: $color; box-shadow: 0 0 0 4px rgba($color, .18), 0 0 26px 14px rgba($color, .22), $shadow-press; }
92%, 100% { background: $stage-paper; border-color: #d8d2c4; box-shadow: $shadow-press; }
}
@keyframes capt-spinner {
0% { opacity: 0; transform: rotate(0deg); }
8% { opacity: 1; }
28% { opacity: 1; transform: rotate(560deg); }
32%, 100% { opacity: 0; transform: rotate(560deg); }
}
@keyframes capt-check {
0%, 28% { opacity: 0; transform: scale(.4); }
40% { opacity: 1; transform: scale(1.15); }
46%, 84% { opacity: 1; transform: scale(1); }
92%, 100% { opacity: 0; transform: scale(.4); }
}
@keyframes capt-badge {
0%, 40% { opacity: 0; transform: translateY(4px); }
48%, 84% { opacity: 1; transform: translateY(0); }
92%, 100% { opacity: 0; transform: translateY(4px); }
}
@media (prefers-reduced-motion: reduce) {
.captcha-card.is-demo .captcha__spinner,
.captcha-card.is-demo .captcha__check,
.captcha-card.is-demo .captcha__badge { animation: none; }
.captcha__input:checked ~ .captcha__box .captcha__check { transition: none; }
}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);
}
.captcha-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.captcha {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 20px;
background: #ffffff;
border: 1.5px solid #d8d2c4;
border-radius: 16px;
box-shadow: 0 6px 12px rgba(23, 20, 26, 0.32);
cursor: pointer;
font-family: inherit;
}
.captcha__input {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
}
.captcha__box {
position: relative;
width: 26px;
height: 26px;
flex: 0 0 auto;
border-radius: 4px;
border: 2px solid #c9c2b2;
background: #ffffff;
}
.captcha__spinner {
position: absolute;
inset: 3px;
border-radius: 50%;
border: 3px solid rgba(76, 212, 166, 0.2);
border-top-color: #4cd4a6;
opacity: 0;
}
.captcha__check {
position: absolute;
inset: 0;
color: #4cd4a6;
opacity: 0;
transform: scale(0.4);
}
.captcha__label {
font-size: 13px;
font-weight: 600;
color: #17141a;
}
.captcha__badge {
font-size: 11px;
font-weight: 700;
color: #4cd4a6;
opacity: 0;
transform: translateY(4px);
}
.captcha__input:checked ~ .captcha__box .captcha__check {
opacity: 1;
transform: scale(1);
transition: opacity 220ms ease-out, transform 220ms cubic-bezier(0.2, 1.4, 0.4, 1);
}
.captcha__input:checked ~ .captcha__box {
border-color: #4cd4a6;
}
.captcha__input:focus-visible ~ .captcha__box {
outline: 2px solid #4cd4a6;
outline-offset: 2px;
}
.captcha-card.is-demo .captcha {
animation: capt-card 2s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
.captcha-card.is-demo .captcha__spinner {
animation: capt-spinner 2s linear infinite;
}
.captcha-card.is-demo .captcha__check {
animation: capt-check 2s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
.captcha-card.is-demo .captcha__badge {
animation: capt-badge 2s ease-out infinite;
}
@keyframes capt-card {
0%, 28% {
background: #ffffff;
border-color: #d8d2c4;
box-shadow: 0 6px 12px rgba(23, 20, 26, 0.32);
}
40%, 84% {
background: rgb(91.5764705882%, 97.9764705882%, 95.8117647059%);
border-color: #4cd4a6;
box-shadow: 0 0 0 4px rgba(76, 212, 166, 0.18), 0 0 26px 14px rgba(76, 212, 166, 0.22), 0 6px 12px rgba(23, 20, 26, 0.32);
}
92%, 100% {
background: #ffffff;
border-color: #d8d2c4;
box-shadow: 0 6px 12px rgba(23, 20, 26, 0.32);
}
}
@keyframes capt-spinner {
0% {
opacity: 0;
transform: rotate(0deg);
}
8% {
opacity: 1;
}
28% {
opacity: 1;
transform: rotate(560deg);
}
32%, 100% {
opacity: 0;
transform: rotate(560deg);
}
}
@keyframes capt-check {
0%, 28% {
opacity: 0;
transform: scale(0.4);
}
40% {
opacity: 1;
transform: scale(1.15);
}
46%, 84% {
opacity: 1;
transform: scale(1);
}
92%, 100% {
opacity: 0;
transform: scale(0.4);
}
}
@keyframes capt-badge {
0%, 40% {
opacity: 0;
transform: translateY(4px);
}
48%, 84% {
opacity: 1;
transform: translateY(0);
}
92%, 100% {
opacity: 0;
transform: translateY(4px);
}
}
@media (prefers-reduced-motion: reduce) {
.captcha-card.is-demo .captcha__spinner,
.captcha-card.is-demo .captcha__check,
.captcha-card.is-demo .captcha__badge {
animation: none;
}
.captcha__input:checked ~ .captcha__box .captcha__check {
transition: none;
}
}import { useState } from "react";
import styles from "./CaptchaVerifyCheckbox.module.scss";
export type CaptchaVerifyCheckboxProps = {
label?: string;
onVerify?: (checked: boolean) => void;
};
/** 캡차 확인 체크박스 — 체크하면 즉시 확인 배지가 뜬다 */
export default function CaptchaVerifyCheckbox({
label = "I'm not a robot",
onVerify,
}: CaptchaVerifyCheckboxProps) {
const [checked, setChecked] = useState(false);
const toggle = () => {
const next = !checked;
setChecked(next);
onVerify?.(next);
};
return (
<div className={styles.card}>
<label className={styles.captcha}>
<input
className={styles.input}
type="checkbox"
checked={checked}
onChange={toggle}
/>
<span className={checked ? `${styles.box} ${styles.boxChecked}` : styles.box}>
<svg className={styles.check} viewBox="0 0 256 256" aria-hidden="true">
<g fill="currentColor">
<path d="M224 128a96 96 0 1 1-96-96a96 96 0 0 1 96 96" opacity=".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 11.32 0M232 128A104 104 0 1 1 128 24a104.11 104.11 0 0 1 104 104m-16 0a88 88 0 1 0-88 88a88.1 88.1 0 0 0 88-88" />
</g>
</svg>
</span>
<span className={styles.label}>{label}</span>
</label>
{checked && <span className={styles.badge}>Verified</span>}
</div>
);
}02 캡차 확인 체크박스 — 측정 누적면적 13.285% · 움직인 프레임 14/23GODRICH 원작 · zip 에 SCSS·React 포함 · 본인인증 화면 UI 9가지 — 그대로 복사하는 인증 카드
이 데모가 속한 글
본인인증 화면 UI 9가지 — 그대로 복사하는 인증 카드본인인증 화면은 사용자가 본인임을 증명하기 위해 권한 요청부터 인증번호 입력, 로그아웃 확인까지 거치는 일련의 UI 조각을 말합니다. 아래 아홉 개는 서버 쪽 인증 로직이 아니라 이 흐름에서 반복되는 화면 패턴만 애 이 글의 데모 전부 보기


