GODRICH175

CSS 유효성 검사 애니메이션 9종 — 폼 에러·성공 표시

CSS 유효성 검사 애니메이션은 폼에 입력한 값이 맞는지 틀린지를 흔들림이 아니라 색·아이콘·글로우 같은 다른 신호로 알려주는 움직임입니다. 아래 아홉 개는 회원가입 폼에서 실제로 일어나는 순서대로 골라 복붙 한 번으로 적용할 수 있게 정리했습니다.

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

아홉 개는 인기 순이 아니라 회원가입 폼 디자인에서 사용자가 폼을 채우며 실제로 마주치는 순서를 따라갔습니다. 타이핑하는 동안 실시간으로 뜨는 메시지(01)에서 아이디 사용 가능 여부(02)로, 검증을 통과한 필드가 떠오르는 신호(03)와 저장 완료 배지(04)를 지나, 계속 남아있는 오류(05)와 진행 중 상태를 보여주는 아이콘(06)을 거쳐, 비밀번호 강도(07)와 제출 버튼의 전체 여정(08), 마지막은 움직임을 줄인 사람에게도 신호가 남는 문제(09) 순서입니다.

01실시간 인라인 에러 메시지

폼 유효성 검사에서 무엇이 보이나

이메일 칸에 형식이 틀린 값을 넣어 두면 그 아래 자리를 미리 잡아 둔 문구가 살짝 위에서 미끄러져 내려오다가 다시 숨습니다.

opacitytranslateY2s 루프

코드에서 볼 곳

opacitytranslateY만 쓴 키프레임 하나뿐이라 문구가 나타나는 동안 옆의 입력창 높이는 전혀 흔들리지 않습니다.

.fv1__msg {
  opacity: 0;
  transform: translateY(-6px);
}
@keyframes fv1-in {
  0%, 6% { opacity: 0; transform: translateY(-6px); }
  28%, 78% { opacity: 1; transform: translateY(0); }
  100% { opacity: 0; transform: translateY(-6px); }
}
.fv1.is-demo .fv1__msg { animation: fv1-in $duration $easing infinite; }

02라벨 상태 색 크로스페이드

화면에서 일어나는 일

닉네임 입력창 위 라벨이 회색 “닉네임”에서 민트색 “사용할 수 있는 닉네임이에요”로 바뀝니다.

opacity 크로스페이드absolute 겹침2s 루프

코드 읽는 법

color 자체를 전환하는 게 아니라 같은 자리에 겹쳐 둔 라벨 두 장이 opacity로 자리를 주고받는 크로스페이드입니다.

@keyframes fv2-ok {
  0%, 20% { opacity: 0; }
  40%, 90% { opacity: 1; }
  100% { opacity: 0; }
}
@keyframes fv2-neutral {
  0%, 20% { opacity: .6; }
  40%, 90% { opacity: 0; }
  100% { opacity: .6; }
}
.fv2.is-demo .fv2__label--ok { animation: fv2-ok $duration $easing infinite; }
.fv2.is-demo .fv2__label--neutral { animation: fv2-neutral $duration $easing infinite; }

03필드 검증 글로우 링

어떤 움직임인가

검증을 통과한 입력창 뒤로 은은한 민트색 링이 부풀어 오르듯 떠오릅니다.

box-shadow 레이어scale 펄스opacity

어디를 바꾸면 되나

입력창 뒤에 포갠 box-shadow 레이어가 opacity 0→1과 scale(.94→1.04)를 함께 써서 만든 효과입니다.

.fv3__glow {
  position: absolute;
  inset: -6px;
  border-radius: $r-control;
  box-shadow: 0 0 0 4px rgba(76, 212, 166, .55);
  opacity: 0;
  transform: scale(.94);
}
@keyframes fv3-glow {
  0%, 15% { opacity: 0; transform: scale(.94); }
  45% { opacity: 1; transform: scale(1.04); }
  75%, 100% { opacity: 0; transform: scale(.94); }
}
.fv3.is-demo .fv3__glow { animation: fv3-glow $duration $easing infinite; }

04성공 배지 바운스 체크

무엇이 보이나

저장이 끝나면 원형 배지가 튀어 오르듯 나타나고, 그 안 체크 표시가 선을 긋듯 완성됩니다.

scale 바운스stroke-dashoffsetopacity

코드에서 볼 곳

배지는 scale(.4→1.12→1)로 바운스하고, 체크는 SVG polylinestroke-dashoffset을 24에서 0으로 줄여 그립니다.

@keyframes fv4-badge {
  0%, 10% { opacity: 0; transform: scale(.4); }
  35% { opacity: 1; transform: scale(1.12); }
  50% { transform: scale(1); }
  85%, 100% { opacity: 1; transform: scale(1); }
}
@keyframes fv4-draw {
  0%, 30% { stroke-dashoffset: 24; }
  55%, 100% { stroke-dashoffset: 0; }
}
.fv4__path { stroke-dasharray: 24; stroke-dashoffset: 24; }
.fv4.is-demo .fv4__badge { animation: fv4-badge $duration $easing infinite; }
.fv4.is-demo .fv4__path { animation: fv4-draw $duration linear infinite; }

05오류 필드 브리딩 링

화면에서 일어나는 일

052편의 흔들림과 달리 이 필드는 전혀 움직이지 않습니다.

box-shadow 브리딩opacityscale

코드 읽는 법

둘레의 연한 코럴색 링이 opacityscale(.97↔1.03) 사이를 오가며 천천히 숨 쉬듯 반복해, 사라지지 않는 오류를 계속 알립니다.

.fv5__ring {
  position: absolute;
  inset: -5px;
  border-radius: $r-control;
  box-shadow: 0 0 0 3px $error-soft;
  opacity: 0;
  transform: scale(.97);
}
@keyframes fv5-breathe {
  0%, 100% { opacity: 0; transform: scale(.97); }
  50% { opacity: 1; transform: scale(1.03); }
}
.fv5.is-demo .fv5__ring { animation: fv5-breathe $duration $easing infinite; }

06상태 아이콘 스피너→체크·엑스 모핑

어떤 움직임인가

아이디 중복을 확인하는 동안 입력창 오른쪽 아이콘이 돌아가는 스피너에서 민트색 체크로, 다시 코럴색 엑스로 자리를 넘겨줍니다.

rotateopacity 크로스페이드svg 3겹

어디를 바꾸면 되나

세 아이콘을 같은 자리에 겹쳐 두고 opacity 크로스페이드로 순서를 만든 뒤, 스피너에만 rotate 애니메이션을 얹었습니다.

@keyframes fv6-spinner-show { 0%, 32% { opacity: 1; } 38%, 100% { opacity: 0; } }
@keyframes fv6-check-show { 0%, 32% { opacity: 0; } 40%, 62% { opacity: 1; } 70%, 100% { opacity: 0; } }
@keyframes fv6-cross-show { 0%, 62% { opacity: 0; } 70%, 96% { opacity: 1; } 100% { opacity: 0; } }
.fv6.is-demo .fv6__spinner {
  animation: fv6-spin $dur-base linear infinite,
             fv6-spinner-show $duration $easing infinite;
}
.fv6.is-demo .fv6__check { animation: fv6-check-show $duration $easing infinite; }
.fv6.is-demo .fv6__cross { animation: fv6-cross-show $duration $easing infinite; }

07비밀번호 강도 3단 채움 바

무엇이 보이나

같은 트랙 위에 약함·보통·강함 막대 세 개가 겹쳐 있습니다.

scaleXopacity 단계 교체transform-origin:left

코드에서 볼 곳

각 막대는 scaleX(transform-origin: left)로 자라난 뒤 opacity로 다음 단계에 자리를 넘겨줘, 색이 서서히 바뀌는 게 아니라 막대 자체가 교체되는 것처럼 보입니다.

.fv7__fill {
  position: absolute; left: 0; top: 0; bottom: 0; width: 100%;
  transform-origin: left center;
  transform: scaleX(0);
  opacity: 0;
}
@keyframes fv7-strong {
  0%, 68% { transform: scaleX(.64); opacity: 0; }
  78%, 96% { transform: scaleX(1); opacity: 1; }
  100% { opacity: 0; transform: scaleX(0); }
}
.fv7.is-demo .fv7__fill--strong { animation: fv7-strong $duration $easing infinite; }

08제출 버튼 로딩→성공 모핑

화면에서 일어나는 일

버튼 하나가 눌리는 동작(scale(1→.96→1))부터 로딩 스피너, 민트색 체크까지 한 번의 루프 안에서 순서대로 보여줍니다.

scalerotateopacity 크로스페이드

코드 읽는 법

라벨이 opacity로 숨은 자리에 스피너가 rotate로 돌다가, 다시 opacity·scale 크로스페이드로 체크에 자리를 넘깁니다.

@keyframes fv8-press { 0%, 8% { transform: scale(1); } 14% { transform: scale(.96); } 22%, 100% { transform: scale(1); } }
@keyframes fv8-check-show {
  0%, 66% { opacity: 0; transform: scale(.5); }
  74% { opacity: 1; transform: scale(1.15); }
  86%, 100% { opacity: 1; transform: scale(1); }
}
.fv8.is-demo { animation: fv8-press $duration $easing infinite; }
.fv8.is-demo .fv8__check { animation: fv8-check-show $duration $easing infinite; }

09모션 축소 시 정지 상태로 대체

어떤 움직임인가

평소엔 상태 아이콘이 scale(.5→1.15→1)opacity로 팝인했다가 사라지는 루프지만, 시스템에서 동작 줄이기를 켜면 애니메이션이 전부 꺼지고 체크와 문구가 움직임 없이 마지막 상태로 고정됩니다.

scale+opacity 팝prefers-reduced-motion정지 상태 고정

어디를 바꾸면 되나

@keyframes fv9-pop {
  0%, 10% { opacity: 0; transform: scale(.5); }
  40% { opacity: 1; transform: scale(1.15); }
  55%, 85% { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(.5); }
}
.fv9.is-demo .fv9__status { animation: fv9-pop $duration $easing infinite; }
@media (prefers-reduced-motion: reduce) {
  .fv9.is-demo .fv9__status { animation: none !important; opacity: 1; transform: scale(1); }
}

어디서 깨지는가 — 크로스페이드 함정

02번을 만들면서 실제로 겪은 함정입니다. 중립 라벨과 성공 라벨을 opacity로 교차시킬 때 animation-direction: reverse로 같은 키프레임 하나를 거울처럼 되돌려 쓰면 코드가 짧아져서 편할 것 같았습니다. 그런데 reverse는 진행률(0~100%)만 뒤집을 뿐이라, 두 라벨의 키프레임이 대칭이 아니면 루프 중간 지점 근처에서 둘 다 opacity가 1이 되는 순간이 생겨 글자가 겹쳐 보였습니다. 렌더된 프레임을 한 장씩 열어보고서야 발견했고, 고친 방법은 reverse를 버리고 opacity 값이 정확히 서로 반대인 두 번째 @keyframes(fv2-neutral)를 새로 만든 것입니다. 아홉 개 전체의 vanilla·React 코드가 든 압축 파일 비밀번호는 mk7s3ujk이고, 02번 폴더의 두 @keyframes를 나란히 놓고 보면 이 차이가 바로 보입니다.

접근성

아홉 개 모두 prefers-reduced-motion: reduce에서 움직임을 끕니다. 다만 03·04·07·08처럼 모양이 바뀌는 것 자체가 정보인 항목은 애니메이션만 끄면 최종 상태 자체가 안 보일 수 있어, 이런 항목은 전환 없이 곧바로 마지막 상태(글로우가 켜진 채, 체크가 다 그려진 채, 막대가 끝까지 채워진 채)로 표시하도록 했습니다. MDN의 prefers-reduced-motion 문서에 따르면 이 값은 운영체제 설정을 그대로 읽어오므로, 움직임을 줄여도 검증 결과라는 정보 자체는 항상 남겨야 합니다. transform·opacity만 움직이는 이유는 web.dev의 고성능 애니메이션 가이드에도 정리되어 있듯, 레이아웃을 다시 계산하지 않고 합성 레이어에서만 처리되기 때문입니다.

흔들림으로 오류를 알리는 쪽이 궁금하면 CSS 흔들리는 애니메이션 9종을, 체크박스·라디오처럼 다른 입력 요소의 검증 표시는 CSS 체크박스 커스텀 9종에서 볼 수 있습니다.

FAQ

회원가입 유효성 검사 애니메이션은 왜 색상 전환 대신 opacity 크로스페이드를 쓰나요?

colorborder-color를 직접 전환하면 브라우저가 두 색 사이 값을 매 프레임 계산해야 해서 연산이 늘고, 다크모드처럼 색 자체가 CSS 변수인 화면에서는 전환 중간값이 이상하게 보일 수 있습니다. 같은 자리에 최종 색을 가진 요소 두 개를 겹쳐 opacity만 바꾸면 계산이 훨씬 가볍고 색 자체는 처음부터 확정된 값입니다.

체크 아이콘을 stroke-dashoffset 대신 두 막대 회전으로 그려도 되나요?

가능하지만 두 막대가 정확히 같은 꼭짓점에서 시작하지 않으면 체크 모양이 사선 한 줄로 어긋나 보이는 문제가 흔합니다. stroke-dasharray·stroke-dashoffset으로 SVG polyline 하나를 그리면 시작점 어긋남 문제 자체가 없어 04번처럼 더 안전합니다.

아홉 개를 한 폼에 전부 같이 써도 되나요?

되지만 필드마다 05(브리딩 링)·03(글로우 링)처럼 항상 켜져 있는 지속 애니메이션을 동시에 여러 개 두면 화면이 산만해집니다. 02·06처럼 상태가 바뀔 때만 잠깐 움직이는 것과 05처럼 오류가 남아있는 동안 계속 도는 것을 구분해서, 한 화면엔 지속 애니메이션을 하나 이하로 두는 걸 권합니다.

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.