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

CSS 흔들리는 애니메이션 9종 — 폼 에러 표시

비밀번호를 잘못 입력했을 때 입력창이 좌우로 흔들리는 걸 본 적 있으신가요? 그 동작이 바로 css 흔들리는 애니메이션입니다 — 자바스크립트 없이 CSS 코드 몇 줄만으로 폼이 틀렸다는 걸 눈에 띄게 알려줍니다.

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

아홉 개는 인기 순이 아니라 오류가 어디서 나느냐를 따라간 순서입니다. 입력값 자체가 틀렸을 때(01~03)에서 화면 요소만 반짝이는 절제된 신호(04)로, 그다음은 비밀번호처럼 더 무거운 실패(05~06)를 거쳐, 브라우저가 스스로 처리하는 경우(07)와 같은 실패가 반복될 때(08), 마지막은 움직임을 줄인 사람에게도 신호가 남아야 하는 문제(09) 순입니다. 틀렸다고 알리는 대신 맞는 곳을 짚어 주는 움직임이 필요하면 텍스트 강조 애니메이션이 그 반대 신호를 담당하고, 에러 색을 빛으로 키우는 쪽은 CSS 글로우 효과가 가져갑니다.

01좌우 흔들림

css shake div 는 무엇을 흔드나

이메일 칸에 형식이 틀린 값을 넣고 다른 곳을 클릭하면 입력창 전체가 좌우로 짧게 흔들리다 멈춥니다.

좌우 흔들림
새 창에서 열기

01 · 좌우 흔들림 — 측정 누적면적 0.494% · 움직인 프레임 8/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

translateX@keyframes500ms

어떻게 만들었나

translateX 값을 -7px→7px→-5px→3px→0px 순서로 나열해 진폭이 갈수록 줄어드는 @keyframes 하나가 전부라, 실제 떨림처럼 보입니다.

@keyframes sh-shake {
  0%, 22%, 100% { transform: translateX(0); }
  2%  { transform: translateX(-7px); }
  4%  { transform: translateX(7px); }
  6%  { transform: translateX(-7px); }
  8%  { transform: translateX(7px); }
  10% { transform: translateX(-5px); }
  12% { transform: translateX(5px); }
  14% { transform: translateX(-3px); }
  16% { transform: translateX(3px); }
}
.sh__input.sh--invalid { animation: sh-shake $duration $easing; }

02비틀거림 회전

어떻게 보이나

약관 체크박스를 누르지 않고 제출을 누르면 체크박스와 문구가 함께 담긴 줄 전체가 좌우로 기우뚱거립니다.

비틀거림 회전
새 창에서 열기

02 · 비틀거림 회전 — 측정 누적면적 1.942% · 움직인 프레임 22/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

rotatetransform-origin600ms

코드 읽는 법

rotate 각도만 바꾸는 @keyframes 라 체크박스 모양 자체는 전혀 건드리지 않습니다.

@keyframes wg-wiggle {
  0%, 100% { transform: rotate(0); }
  15% { transform: rotate(-4deg); }
  30% { transform: rotate(4deg); }
  45% { transform: rotate(-3deg); }
  60% { transform: rotate(3deg); }
  75% { transform: rotate(-1.5deg); }
  90% { transform: rotate(1.5deg); }
}
.wg__row.wg--wiggle { animation: wg-wiggle $duration $easing; }

03젤리처럼 출렁

언제 쓰나

저장 버튼을 눌렀는데 실패하면 버튼이 젤리처럼 출렁이다 가라앉습니다.

젤리처럼 출렁
새 창에서 열기

03 · 젤리처럼 출렁 — 측정 누적면적 1.069% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

skewXskewY800ms

코드의 요점

skewX와 skewY를 같은 키프레임 안에서 함께 틀어 비스듬한 두 방향으로 동시에 찌그러지는 게 핵심입니다.

@keyframes je-jello {
  0%, 100% { transform: none; }
  15% { transform: skewX(-9deg) skewY(-9deg); }
  30% { transform: skewX(7deg) skewY(7deg); }
  45% { transform: skewX(-5deg) skewY(-5deg); }
  60% { transform: skewX(3deg) skewY(3deg); }
  75% { transform: skewX(-2deg) skewY(-2deg); }
  90% { transform: skewX(1deg) skewY(1deg); }
}
.je__btn.je--jello { animation: je-jello $duration $easing; }

04오류 테두리 플래시

어떤 화면인가

카드 번호 칸 자체는 움직이지 않고, 뒤에 겹쳐 둔 빨간 테두리 레이어만 두 번 반짝입니다.

오류 테두리 플래시
새 창에서 열기

04 · 오류 테두리 플래시 — 측정 누적면적 0.869% · 움직인 프레임 16/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

opacityinset: 0500ms

동작 원리

입력창까지 흔들리면 옆에 나란히 있는 다른 칸들도 함께 흔들리는 것처럼 보일 수 있어, opacity만 바꿔 레이아웃은 그대로 둡니다.

.bf__ring {
  position: absolute;
  inset: -$sp-1;
  border: 2px solid $color;
  border-radius: $r-sm;
  opacity: 0;
}
@keyframes bf-flash {
  0%, 100% { opacity: 0; }
  15% { opacity: 1; }
  30% { opacity: 0; }
  45% { opacity: 1; }
  60% { opacity: 0; }
}
.bf__ring.bf--flash { animation: bf-flash $duration $easing; }

05입력창 헤드셰이크

무엇이 움직이나

비밀번호 확인 칸이 일치하지 않으면 입력창이 마치 고개를 젓듯 입체적으로 살짝 돌아갑니다.

입력창 헤드셰이크
새 창에서 열기

05 · 입력창 헤드셰이크 — 측정 누적면적 2.372% · 움직인 프레임 15/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

rotateYperspective900ms

만드는 방식

translateX와 rotateY를 같은 키프레임에서 함께 쓰고, 부모에 perspective를 줘야 이 입체감이 삽니다.

.hs { perspective: 500px; }
@keyframes hs-shake {
  0% { transform: translateX(0); }
  6.5% { transform: translateX(-6px) rotateY(-9deg); }
  18.5% { transform: translateX(5px) rotateY(7deg); }
  31.5% { transform: translateX(-3px) rotateY(-5deg); }
  43.5% { transform: translateX(2px) rotateY(3deg); }
  50%, 100% { transform: translateX(0); }
}
.hs__input.hs--shake { animation: hs-shake $duration $easing; }

06경고 아이콘 바운스

어디에 쓰나

느낌표가 그려진 원형 아이콘이 튀어 올랐다가 착지하며 시선을 오류 메시지 쪽으로 끕니다.

경고 아이콘 바운스
새 창에서 열기

06 · 경고 아이콘 바운스 — 측정 누적면적 0.726% · 움직인 프레임 18/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

scaletranslateY700ms

왜 이렇게 만들었나

scale과 translateY를 같이 걸어 통통 튀는 인상을 만들고, 아이콘은 장식이라 aria-hidden으로 처리했습니다.

@keyframes ib-bounce {
  0%, 100% { transform: translateY(0) scale(1); }
  20% { transform: translateY(-10px) scale(1.15); }
  40% { transform: translateY(0) scale(.95); }
  55% { transform: translateY(-4px) scale(1.05); }
  70% { transform: translateY(0) scale(1); }
}
.ib__icon.ib--bounce { animation: ib-bounce $duration $easing; }

07:user-invalid 자동 흔들림

눈에 보이는 것

CSS 쪽 코드는 JS를 한 줄도 안 쓰고, 사용자가 이메일 칸에 값을 넣고 벗어난 뒤 형식이 틀렸을 때만 브라우저가 스스로 흔들어 줍니다.

:user-invalid 자동 흔들림
새 창에서 열기

07 · :user-invalid 자동 흔들림 — 측정 누적면적 0.507% · 움직인 프레임 8/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

:user-invalidpattern500ms

구현의 핵심

input:user-invalid 의사 클래스 하나에 animation을 걸면 끝이고, 값을 만지기 전이나 페이지가 막 열렸을 때는 조용합니다.

.ui__input:user-invalid {
  border-color: $color;
  animation: ui-shake $duration $easing;
}
@keyframes ui-shake {
  0%, 22%, 100% { transform: translateX(0); }
  2%  { transform: translateX(-7px); }
  4%  { transform: translateX(7px); }
  6%  { transform: translateX(-7px); }
  8%  { transform: translateX(7px); }
  10% { transform: translateX(-5px); }
  12% { transform: translateX(5px); }
  14% { transform: translateX(-3px); }
  16% { transform: translateX(3px); }
}

081회 재생 리셋 패턴

어떤 상황인가

"다시 확인"을 연달아 눌러도 매번 처음부터 새로 흔들립니다.

1회 재생 리셋 패턴
새 창에서 열기

08 · 1회 재생 리셋 패턴 — 측정 누적면적 0.56% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

animationendreflowJS 6줄

무엇이 핵심인가

같은 클래스를 지우고 곧바로 다시 붙이면 브라우저가 재생 중이던 애니메이션으로 착각해 무시하기 때문에, 지운 뒤 offsetWidth를 한 번 읽어 강제로 리플로우를 일으키고 나서 다시 붙입니다.

var input = document.getElementById('rs-pw');
document.getElementById('rs-check').addEventListener('pointerdown', function () {
  input.classList.remove('rs--shake');
  void input.offsetWidth;
  input.classList.add('rs--shake');
});

09모션 축소 시 아웃라인 대체

무슨 일이 일어나나

평소에는 01과 같은 방식으로 흔들리지만, 시스템에서 동작 줄이기를 켜면 흔들림 대신 빨간 테두리 링이 두 번 깜빡이는 것으로 바뀝니다.

모션 축소 시 아웃라인 대체
새 창에서 열기

09 · 모션 축소 시 아웃라인 대체 — 측정 누적면적 0.336% · 움직인 프레임 8/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

prefers-reduced-motionrm__ringopacity

짚어 둘 점

움직임을 꺼도 오류가 있다는 신호까지 함께 사라지지 않게 하려는 것입니다.

@media (prefers-reduced-motion: reduce) {
  .rm__textarea.rm--invalid { animation: none !important; }
  .rm__ring.rm--invalid { animation: rm-ring-pulse $dur-loop ease-in-out; }
}
@keyframes rm-ring-pulse {
  0%, 100% { opacity: 0; }
  25% { opacity: 1; }
  50% { opacity: 0; }
  75% { opacity: 1; }
}

어디서 깨지는가 — 함정 하나

무엇이 문제인가

입력창을 통째로 흔들든 css shake text 처럼 글자만 흔들든, 여덟 개(01~07·09)는 CSS 애니메이션이 한 번 재생되면 끝이라는 공통 함정을 가지고 있습니다. 같은 값으로 또 틀려도 클래스가 이미 붙어 있으면 브라우저는 "이미 재생 중이던 애니메이션"으로 보고 다시 틀어 주지 않습니다. 08번 항목이 이 문제를 classList.remove() → offsetWidth 강제 읽기 → classList.add() 세 단계로 고친 코드를 담고 있고, 나머지 항목에도 같은 패턴을 그대로 옮겨 쓸 수 있습니다.

어떻게 피하나

떨림 폭이 커질수록 주변 요소까지 밀려 보이는 이 현상은, 조각들을 일부러 겹쳐 붙이는 콜라주 맥시멀리즘 랜딩에서 레이어 간격을 잡을 때와 같은 계산을 씁니다.

:user-invalid(07)를 쓴다면 이 문제 자체가 없습니다 — 값이 바뀔 때마다 브라우저가 상태를 다시 계산해 스스로 재생하기 때문입니다. 아홉 개 전체 vanilla·React 코드가 든 압축 파일은 3v34eb5k으로 열립니다.

접근성

reduced-motion 에서 남는 것

아홉 개 모두 prefers-reduced-motion: reduce에서 animation: none !important으로 움직임을 끕니다. 다만 04·09처럼 움직임이 유일한 오류 신호인 항목은 애니메이션만 끄면 오류 자체가 안 보일 수 있어, 09는 흔들림 대신 opacity로 깜빡이는 테두리 링을 대신 켜 두고 나머지 항목은 문구 텍스트가 항상 함께 보이게 했습니다. :user-invalid 같은 네이티브 검증 의사 클래스는 MDN 문서에 따르면 Chrome/Edge 119+·Firefox 88+·Safari 16.4+에서 동작하고, 그 이전 버전은 조용히 무시되니 결제·회원가입처럼 놓치면 안 되는 폼은 01·05 같은 클래스 기반 방식을 기본으로 두는 걸 권합니다.

이어서 볼 글

탭이 바뀔 때 인디케이터가 미끄러지는 동작이 궁금하면 CSS 탭 메뉴 전환 애니메이션 9종을, 이 사이트가 오류 신호 말고 또 뭘 다루는지는 CSS 애니메이션 카테고리에서 볼 수 있습니다.

FAQ

흔들림 애니메이션은 왜 opacity나 transform만 써야 하나요?

margin이나 left 같은 속성을 흔들면 브라우저가 매 프레임 레이아웃을 다시 계산해야 해서 버벅이고, 흔들리는 요소 옆 칸까지 따라 밀립니다. transform과 opacity는 레이아웃을 다시 계산하지 않고 화면에 이미 그려진 픽셀만 옮기거나 흐리게 해서 끝나 훨씬 매끄럽습니다.

:user-invalid랑 :invalid는 뭐가 다른가요?

:invalid는 값이 비어 있거나 형식이 틀리면 페이지가 열리자마자 곧바로 매치됩니다. :user-invalid는 사용자가 실제로 값을 만지고 포커스를 벗어난 뒤에만 매치되어서, 아직 아무것도 입력 안 한 빈칸을 열자마자 빨갛게 흔드는 실수를 막아 줍니다.

흔들리는 효과를 한 번만 재생하려면 어떻게 하나요?

08번처럼 클래스를 지우고 offsetWidth를 한 번 읽어 강제로 리플로우를 일으킨 뒤 다시 클래스를 붙이면 같은 클래스라도 매번 처음부터 재생됩니다. React라면 이 대신 요소의 key를 바꿔 통째로 새로 마운트하는 쪽이 더 자연스럽습니다. 흔들림 하나가 아니라 입력 검증 전체의 성공·실패 표시가 필요하다면 CSS 유효성 검사 애니메이션이 그 묶음입니다.

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

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