GODRICH

CSS 키프레임 애니메이션 9종 — 한 줄 복붙으로

CSS 키프레임 애니메이션은 @keyframes로 만든 움직임을 클래스 하나에 담아 어떤 요소에든 그대로 붙여 쓰는 방식입니다. 뜨기·튀기·돌기·펄스·흔들기·비틀기·깜빡임·심장박동·젤리, 아홉 가지를 한 번에 모았고 전부 transform과 opacity 두 속성만 씁니다.

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

아홉 개는 이름 가나다순이 아니라 한 요소가 화면에서 맡는 역할 순서로 놓았습니다. 존재를 알리고(01~02), 대기 중임을 말하고(03~04), 주의를 끌고(05~06), 상태가 바뀌었음을 알립니다(07~09). 아래 표로 먼저 훑고, 각 항목 아래에서 코드를 확인하세요.

클래스 무엇이 움직이나 duration 반복
.a-float translateY 3초 무한
.a-bounce translateY 1초 무한
.a-spin rotate 1초 무한
.a-pulse scale·opacity 2초 무한
.a-shake translateX 0.5초 한 번(트리거)
.a-wobble translateX·rotate 1초 무한
.a-flash opacity 1초 무한
.a-heartbeat scale 1.2초 무한
.a-jelly scaleX·scaleY 0.6초 한 번(트리거)

01둥둥 떠 있는

.a-float 하나만 붙이면 요소가 위아래로 천천히 오갑니다. 일러스트나 아이콘 배지처럼 화면 한구석에서 계속 존재를 알려야 하는 요소에 씁니다.

translateYease-in-out3초
$duration: 3s;
$easing: ease-in-out;

.a-float { animation: float $duration $easing infinite; }

@keyframes float {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-16px); }
}

02통통 튀는

.a-bounce는 바닥에 튕기는 리듬으로 짧게 내려갔다 크게 튀어 오르길 반복합니다. 스크롤 유도 화살표나 "더 보기" 아이콘처럼 눌러도 되는 자리라는 걸 몸짓으로 알려야 할 때 씁니다.

translateY바운스 이징1초
$duration: 1s;
$easing: cubic-bezier(.28, .84, .42, 1);

.a-bounce { animation: bounce $duration $easing infinite; }

@keyframes bounce {
  0%, 100% { transform: translateY(0); }
  30%      { transform: translateY(-4px); }
  55%      { transform: translateY(10px); }
  75%      { transform: translateY(-2px); }
  90%      { transform: translateY(4px); }
}

03회전

.a-spin은 로딩 스피너 전용이 아닙니다. 새로고침 버튼이나 동기화 아이콘처럼 "지금 진행 중"을 말해야 하는 어떤 아이콘에도 그대로 붙일 수 있는 범용 회전 클래스입니다.

rotatelinear1초
$duration: 1s;
$easing: linear;         // 가감속 없이 일정해야 끊겨 보이지 않는다

.a-spin { animation: spin $duration $easing infinite; }

@keyframes spin {
  to { transform: rotate(360deg); }
}

04펄스

.a-pulse는 scale과 opacity를 함께 오가며 레이아웃을 다시 그리지 않고도 시선을 붙잡습니다. 라이브 상태 점, 신규 기능 배지, 강조가 필요한 CTA 버튼에 씁니다.

scaleopacity2초
$duration: 2s;
$easing: ease-out;

.a-pulse { animation: pulse $duration $easing infinite; }

@keyframes pulse {
  0%, 100% { transform: scale(1);    opacity: 1; }
  50%      { transform: scale(1.06); opacity: .92; }
}

05흔들리는

.a-shake는 폼 검증 실패 전용이 아닙니다. "이 요소를 봐 달라"는 범용 주의 환기용으로, 짧게 왕복하고 스스로 멈춥니다. 새 기능 배지를 흔들거나 튜토리얼에서 한 지점을 짚을 때 씁니다.

translateX6회 왕복0.5초
$duration: 500ms;
$easing: cubic-bezier(.36, .07, .19, .97);
$amount: 8px;

.a-shake.is-shaking { animation: shake $duration $easing 1; }

@keyframes shake {
  10%, 90% { transform: translateX($amount * -.5); }
  20%, 80% { transform: translateX($amount); }
  30%, 70% { transform: translateX($amount * -1); }
  40%, 60% { transform: translateX($amount * .7); }
  50%      { transform: translateX($amount * -.7); }
}

06비틀거림

.a-wobble은 translateX와 rotate를 같은 keyframes 안에서 서로 다른 지점에 겹쳐 05 흔들림보다 불규칙하게 움직입니다. 드래그 가능한 카드나 재정렬되는 리스트 항목이 "저를 옮겨보세요"라고 말할 때 씁니다.

translateX+rotate다단 keyframes1초
$duration: 1s;
$easing: ease-in-out;

.a-wobble { animation: wobble $duration $easing infinite; }

@keyframes wobble {
  0%, 100% { transform: translateX(0)    rotate(0deg); }
  15%      { transform: translateX(-6px) rotate(-4deg); }
  30%      { transform: translateX(5px)  rotate(3deg); }
  45%      { transform: translateX(-4px) rotate(-2deg); }
  60%      { transform: translateX(3px)  rotate(2deg); }
  75%      { transform: translateX(-2px) rotate(-1deg); }
}

07깜빡임

.a-flash는 opacity를 두 번 끊어 깜빡이되 요소를 완전히 지우지는 않습니다. 긴급 공지 아이콘, 라이브 방송 중 표시 점처럼 확실히 눈에 띄어야 하는 곳에 씁니다.

opacity2단 점멸1초
$duration: 1s;
$easing: ease-in-out;

.a-flash { animation: flash $duration $easing infinite; }

@keyframes flash {
  0%, 50%, 100% { opacity: 1; }
  25%, 75%      { opacity: .15; }
}

08심장박동

.a-heartbeat는 scale을 짧게 두 번 튀긴 뒤 쉬는 구간을 둬 실제 심장처럼 불균등한 리듬으로 뛰게 합니다. 좋아요 아이콘의 대기 상태나 실시간 모니터링 대시보드의 상태 점에 씁니다.

scale 2단불균등 리듬1.2초
$duration: 1.2s;
$easing: ease-in-out;

.a-heartbeat { animation: heartbeat $duration $easing infinite; }

@keyframes heartbeat {
  0%, 100% { transform: scale(1); }
  14%      { transform: scale(1.24); }
  28%      { transform: scale(1); }
  42%      { transform: scale(1.18); }
  70%      { transform: scale(1); }
}

09젤리처럼 출렁

.a-jelly는 scaleX와 scaleY를 서로 반대 방향으로 움직여 눌렸다 놓인 순간 젤리처럼 출렁이다 제 모양으로 돌아옵니다. 장바구니 담기 완료나 클릭 후 통통 튀는 버튼 피드백에 씁니다.

scale(1.25, .75) 어긋남젤리 이징0.6초
$duration: 600ms;
$easing: cubic-bezier(.68, -0.55, .27, 1.55);

.a-jelly.is-jellying { animation: jelly $duration $easing 1; }

@keyframes jelly {
  0%   { transform: scale(1, 1); }
  20%  { transform: scale(1.25, .75); }
  40%  { transform: scale(.75, 1.25); }
  60%  { transform: scale(1.15, .85); }
  80%  { transform: scale(.95, 1.05); }
  100% { transform: scale(1, 1); }
}

animation 축약형은 왜 8개 값이 이 순서인가

animation: 한 줄에 값을 몰아 쓸 때 순서를 외우지 않으면 durationdelay를 헷갈리기 쉽습니다. MDN의 animation 단축 속성 문서가 정리한 공식 순서는 이렇습니다. animation-name은 다른 값과 겹칠 일이 없어 이 편의 코드처럼 맨 앞에 써도 그대로 파싱되지만, MDN은 모호함을 피하려면 맨 뒤에 두길 권장합니다.

자리 하위 속성 이 편의 예 기본값
1 animation-duration 3s 0s
2 animation-timing-function ease-in-out ease
3 animation-delay (없으면 0) 0s
4 animation-iteration-count infinite 1
5 animation-direction (없으면 normal) normal
6 animation-fill-mode (없으면 none) none
7 animation-play-state (없으면 running) running
8 animation-name float none

animation-fill-mode는 애니메이션이 뛰기 전·뛴 뒤에도 keyframes 값을 유지할지 정합니다. 05 흔들림과 09 젤리는 1(한 번만 재생)로 끝나는데, 0%와 100%가 둘 다 원래 모양이라 fill-mode 없이도 자연스럽게 제자리로 돌아옵니다. 만약 keyframes의 마지막 프레임이 원래 모습과 다르다면 forwards를 더해야 애니메이션이 끝난 뒤에도 그 모양이 유지됩니다.

어디서 깨지는가 — 함정 둘

.a-float.a-spin처럼 서로 다른 유틸리티 클래스를 같은 요소에 동시에 붙이면 둘 다 animation 축약 속성을 통째로 선언하기 때문에, CSS 캐스케이드 규칙에 따라 스타일시트에 나중에 정의된 규칙이 앞의 것을 완전히 덮어써 하나만 움직입니다. (class 속성에 적는 순서는 상관없습니다.) 한 요소에 두 움직임을 겹치려면 animation-name: float, spin;처럼 이름을 나열하고 animation-duration·animation-timing-function도 같은 개수로 나열해야 합니다. 이때 두 애니메이션이 같은 속성(transform)을 건드리면 기본값 replace가 서로를 덮어써 결국 하나만 보이는데, animation-composition: add를 주면 두 transform 값을 더해 함께 적용합니다. 이 속성은 2023년 7월부터 주요 브라우저에 널리 퍼졌습니다.

아홉 개 중 04 펄스의 평균 강도(강도_평균)는 30.9로 가장 낮게 측정됐습니다. scale과 opacity를 미세하게만 움직이기 때문인데, 점처럼 작은 요소에 걸면 그 낮은 강도가 더 두드러져 거의 안 보일 수 있습니다. 버튼처럼 면적이 넓은 요소에 걸어야 은은하면서도 확실한 강조로 읽힙니다. 이 값들이 이미 들어간 아홉 개 파일의 압축 비밀번호는 nft4q6u2이고, 아래 버튼으로 받을 수 있습니다.

접근성

시스템에서 동작 줄이기를 켠 사용자를 위해 아홉 개 전부 @media (prefers-reduced-motion: reduce) 안에서 animation: none으로 끕니다. 움직임을 지우되 정보는 남기는 게 원칙이라, 05 흔들림은 배경색으로 대신 알리고 나머지는 애니메이션이 멈춘 정적 상태 그대로 읽을 수 있습니다. 이 미디어 쿼리를 어떤 브라우저가 읽는지는 MDN 문서의 호환성 표에 정리돼 있습니다.

@media (prefers-reduced-motion: reduce) {
  .a-float, .a-bounce, .a-spin, .a-pulse,
  .a-wobble, .a-flash, .a-heartbeat { animation: none; }
  .a-shake.is-shaking, .a-jelly.is-jellying { animation: none; }
}

다른 CSS 애니메이션 글은 CSS 애니메이션 카테고리에 모여 있고, 이 사이트가 무엇을 다루는지는 소개에 있습니다.

FAQ

아홉 개를 한 화면에 다 써도 되나요?

넣을 수는 있지만 한 화면에는 두세 개 이하가 좋습니다. 04 펄스와 07 깜빡임처럼 둘 다 "주목하라"는 신호를 가진 클래스를 한 화면에 여러 개 두면 어느 것도 특별해 보이지 않습니다. 03 회전과 08 심장박동처럼 상태가 다른 것끼리는 같이 둬도 자연스럽습니다.

두 애니메이션을 한 요소에 겹치려면 어떻게 하나요?

클래스 두 개를 그냥 같이 붙이면 스타일시트에 나중 정의된 클래스가 animation 전체를 덮어써 하나만 남습니다. animation-name에 이름을 쉼표로 나열하고, 같은 속성을 건드린다면 animation-composition: addaccumulate로 합치는 방식을 쓰세요. 자세한 예시는 함정 섹션에 있습니다.

React에서는 클래스 토글 대신 뭘 쓰나요?

zip의 react/ 폴더는 duration·easing·color를 props로 받아 CSS 변수로 넘깁니다. 05 흔들림과 09 젤리처럼 한 번만 재생하는 것은 useState로 상태 클래스를 켜고 onAnimationEnd에서 꺼서, 다시 클릭했을 때 재생되게 만들었습니다.

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

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