GODRICH
#css애니메이션#버튼#로딩#전환효과#스크롤#대시보드#실전ui#디자인트렌드

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

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

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

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

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

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

이메일 칸에 형식이 틀린 값을 넣어 두면 그 아래 자리를 미리 잡아 둔 문구가 살짝 위에서 미끄러져 내려오다가 다시 숨습니다. 문구가 나타나는 순간 필드에 translateX가 픽셀 미만으로 잦아드는 감쇠 진동이 한 번 지나가고, 동시에 테두리 밖으로 번지는 글로우 파동이 오류를 한 번 더 짚어줍니다.

실시간 인라인 에러 메시지
새 창에서 열기

01 · 실시간 인라인 에러 메시지 — 측정 누적면적 3.469% · 움직인 프레임 13/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

opacitytranslateY2s 루프

코드에서 볼 곳

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

.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라벨 상태 색 크로스페이드

화면에서 일어나는 일

닉네임 입력창 위 라벨이 회색 “닉네임”에서 민트색 “사용할 수 있는 닉네임이에요”로 바뀝니다. 라벨이 바뀌는 순간 필드에 감쇠 진동 살짝 지나가고 민트 글로우가 한 번 번졌다 잦아듭니다.

라벨 상태 색 크로스페이드
새 창에서 열기

02 · 라벨 상태 색 크로스페이드 — 측정 누적면적 3.418% · 움직인 프레임 10/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

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필드 검증 글로우 링

어떤 움직임인가

검증을 통과한 입력창 뒤로 은은한 민트색 링이 부풀어 오르듯 떠오릅니다. 피크 뒤에는 곧바로 소멸하는 대신 점점 약해지는 2·3차 파동이 잦아들며 사라져, 물결이 가라앉는 여운을 남깁니다.

필드 검증 글로우 링
새 창에서 열기

03 · 필드 검증 글로우 링 — 측정 누적면적 3.533% · 움직인 프레임 11/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

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성공 배지 바운스 체크

무엇이 보이나

저장이 끝나면 원형 배지가 튀어 오르듯 나타나고, 그 안 체크 표시가 선을 긋듯 완성됩니다. 배지 등장에는 rotate 감쇠 진동이 얹혀 좌우로 잦아들며 튀고, 둘레로 글로우 링이 같은 박자로 번집니다.

성공 배지 바운스 체크
새 창에서 열기

04 · 성공 배지 바운스 체크 — 측정 누적면적 3.188% · 움직인 프레임 17/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

scale 바운스stroke-dashoffsetopacity

코드에서 볼 곳

배지는 scale(.4→1.12→1)로 바운스하고, 체크는 SVG polyline의 stroke-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편의 흔들림과 달리 이 필드는 전혀 움직이지 않습니다. 숨 쉬던 링은 정점 뒤 단조롭게 사라지는 게 아니라 점점 약해지는 파동을 남기며 잦아들어, 052의 위치 이동 없이도 물결의 여운을 같은 자리에서 느끼게 합니다.

오류 필드 브리딩 링
새 창에서 열기

05 · 오류 필드 브리딩 링 — 측정 누적면적 2.63% · 움직인 프레임 20/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

box-shadow 브리딩opacityscale

코드 읽는 법

둘레의 연한 코럴색 링이 opacity와 scale(.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상태 아이콘 스피너→체크·엑스 모핑

어떤 움직임인가

아이디 중복을 확인하는 동안 입력창 오른쪽 아이콘이 돌아가는 스피너에서 민트색 체크로, 다시 코럴색 엑스로 자리를 넘겨줍니다. 상태가 바뀌는 두 순간마다 감쇠 진동이 지나가고 해당 상태 색 글로우가 번지는데, 체크에서는 미세하게·엑스에서는 뚜렷하게 세기를 달리해 좋지 않은 소식에 더 힘을 줍니다.

상태 아이콘 스피너→체크·엑스 모핑
새 창에서 열기

06 · 상태 아이콘 스피너→체크·엑스 모핑 — 측정 누적면적 1.374% · 움직인 프레임 20/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

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단 채움 바

무엇이 보이나

같은 트랙 위에 약함·보통·강함 막대 세 개가 겹쳐 있습니다. 각 단계에 도달하는 순간 트랙이 scaleY로 한 번 튀었다 잦아들고, 트랙 바깥 둘레로는 그 단계의 색 글로우가 번지는 파동이 따라옵니다.

비밀번호 강도 3단 채움 바
새 창에서 열기

07 · 비밀번호 강도 3단 채움 바 — 측정 누적면적 1.161% · 움직인 프레임 18/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

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))부터 로딩 스피너, 민트색 체크까지 한 번의 루프 안에서 순서대로 보여줍니다. 체크가 뜨는 순간 버튼 둘레로 민트 글로우 링이 퍼지고, 버튼은 감쇠 rotate 진동으로 한 번 흔들렸다 멈춥니다.

제출 버튼 로딩→성공 모핑
새 창에서 열기

08 · 제출 버튼 로딩→성공 모핑 — 측정 누적면적 4.267% · 움직인 프레임 20/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

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로 팝인했다가 사라지는 루프지만, 시스템에서 동작 줄이기를 켜면 애니메이션이 전부 꺼지고 체크와 문구가 움직임 없이 마지막 상태로 고정됩니다. 팝인 순간엔 아주 옅은 글로우 파동이 한 번 번지는데, 이 파동 역시 동작 줄이기에서는 같이 꺼집니다.

모션 축소 시 정지 상태로 대체
새 창에서 열기

09 · 모션 축소 시 정지 상태로 대체 — 측정 누적면적 0.675% · 움직인 프레임 17/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

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 크로스페이드를 쓰나요?

color나 border-color를 직접 전환하면 브라우저가 두 색 사이 값을 매 프레임 계산해야 해서 연산이 늘고, 다크모드처럼 색 자체가 CSS 변수인 화면에서는 전환 중간값이 이상하게 보일 수 있습니다. 같은 자리에 최종 색을 가진 요소 두 개를 겹쳐 opacity만 바꾸면 계산이 훨씬 가볍고 색 자체는 처음부터 확정된 값입니다. 성공·실패를 사람 대신 표정으로 전하는 쪽은 마스코트 애니메이션 9종이 맡습니다.

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

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

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

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

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

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