같은 자리에서 같은 자리로 요소를 옮겨도 어떤 움직임은 살아 있는 듯 매끄럽고 어떤 움직임은 뻣뻣합니다. 그 차이를 가르는 시간과 곡선을 CSS로 하나씩 만져 보겠습니다. 아래 코드는 그대로 복사해 브라우저에서 확인할 수 있습니다.

85.1 지속시간부터 정한다

움직임의 길이가 어긋나면 아무리 좋은 이징도 어색합니다. 손끝에 곧바로 답하는 반응은 .1s에서 .2s로 짧게, 패널이 열리고 닫히는 전환은 .2s에서 .4s로 조금 여유 있게 잡습니다.


.tap {
transition: transform .12s ease-out;
}
.panel {
transition: opacity .3s ease, transform .3s ease;
}


작은 요소가 짧은 거리를 갈 때는 짧게, 큰 화면이 먼 거리를 가로지를 때는 길게. 모든 움직임에 같은 길이를 붙이면 작은 것은 굼떠 보이고 큰 것은 성급해 보입니다.

85.2 이징 키워드 다섯 가지

같은 시간, 같은 거리라도 속도가 변하는 결에 따라 인상이 갈립니다. 브라우저가 기본으로 주는 키워드 다섯 개를 나란히 놓고 봅니다.


.a { transition: transform .4s linear; }
.b { transition: transform .4s ease; }
.c { transition: transform .4s ease-in; }
.d { transition: transform .4s ease-out; }
.e { transition: transform .4s ease-in-out; }


linear는 처음부터 끝까지 한 속도라 기계적이고, ease-in은 느리게 출발하니 사라지는 것에, ease-out은 빠르게 튀어나오니 나타나는 것에, ease-in-out은 양끝이 부드러우니 자리를 옮기는 것에 어울립니다. 사용자에게 답하는 등장에는 대개 ease-out을 씁니다.

85.3 cubic-bezier로 곡선을 직접 그린다

키워드로 부족하면 네 개의 숫자로 속도 곡선을 직접 그립니다. 앞의 두 값은 시작점의 손잡이, 뒤의 두 값은 끝점의 손잡이입니다.


.smooth {
transition: transform .35s cubic-bezier(0.22, 1, 0.36, 1);
}
.smooth:hover {
transform: translateY(-6px);
}


끝점 손잡이의 세로값을 1에 가깝게 당기면 끝에서 부드럽게 감속합니다. ease 계열이 답답하게 느껴질 때 이렇게 나만의 곡선을 만들어 손맛을 조율합니다.

85.4 steps로 뚝뚝 끊어 움직이기

모든 움직임이 매끄러워야 하는 것은 아닙니다. steps는 구간을 딱딱 끊어 넘겨서 타자기나 스프라이트 같은 효과를 냅니다. 먼저 글자가 한 칸씩 찍히는 타자기입니다.


@keyframes type {
from { width: 0; }
to { width: 22ch; }
}
.typewriter {
width: 22ch;
white-space: nowrap;
overflow: hidden;
border-right: 2px solid #111;
animation: type 2s steps(22) forwards;
}


22칸을 22단계로 끊으니 글자가 한 칸씩 나타납니다. 같은 원리로 이어 붙인 그림을 한 칸씩 밀면 옛날 게임의 걷는 동작이 됩니다.


@keyframes walk {
to { background-position: -1200px 0; }
}
.sprite {
width: 120px;
height: 120px;
background: url(walk.png) 0 0;
animation: walk .8s steps(10) infinite;
}


가로로 이어 붙인 열 장의 그림을 10단계로 끊어 밀면, 사이가 부드럽게 이어지지 않고 한 컷씩 넘어가며 걷는 것처럼 보입니다.

85.5 오버슈트로 스프링 느낌 주기

끝점 손잡이의 세로값을 1보다 크게 잡으면 목적지를 살짝 지나쳤다 되돌아옵니다. 용수철에 매달린 듯 통통 튀는 결입니다.


.spring {
transition: transform .4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.spring:hover {
transform: scale(1.12);
}


1.56이 1을 넘어서기에 살짝 커졌다가 제자리로 돌아옵니다. 경쾌하지만 개성이 강해서 자주 쓰면 산만해지니, 활기를 드러내고 싶은 특별한 자리에만 아껴 놓습니다.

85.6 순차 등장으로 흐름 만들기

여러 요소가 한꺼번에 툭 나타나면 화면이 덜컥합니다. 아주 짧은 시차를 두고 차례차례 떠오르게 하면 시선이 자연스럽게 흐릅니다. nth-child에 지연을 조금씩 더해 줍니다.


@keyframes rise {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
.item {
opacity: 0;
animation: rise .4s ease-out forwards;
}
.item:nth-child(1) { animation-delay: .05s; }
.item:nth-child(2) { animation-delay: .1s; }
.item:nth-child(3) { animation-delay: .15s; }
.item:nth-child(4) { animation-delay: .2s; }


항목마다 0.05s씩 늦게 시작하니 위에서부터 물결처럼 떠오릅니다. 시차가 너무 벌어지면 도리어 굼떠 보이니, 눈이 겨우 알아챌 만큼만 촘촘히 둡니다.

85.7 keyframes로 구간별 타이밍 짜기

transition은 시작과 끝만 정하지만, keyframes는 중간 구간을 백분율로 잡아 움직임의 박자를 마디마다 다르게 새길 수 있습니다. 구간 간격을 고르지 않게 두면 빨라졌다 느려지는 결이 생깁니다.


@keyframes attention {
0% { transform: scale(1); }
20% { transform: scale(1.15); }
40% { transform: scale(0.95); }
60% { transform: scale(1.05); }
100% { transform: scale(1); }
}
.bell {
animation: attention 1s ease-in-out;
}


앞쪽 20퍼센트 안에서 크게 부풀었다가, 뒤로 갈수록 진폭이 잦아들며 제자리를 찾습니다. 구간을 촘촘히 두느냐 성글게 두느냐로 움직임의 리듬을 손끝처럼 다듬을 수 있습니다.

85.8 움직임을 원치 않는 사용자 존중하기

어떤 사용자는 화면이 움직이면 어지럼을 느낍니다. 운영체제에서 움직임 줄이기를 켠 사람에게는 지속시간을 거의 0으로 눌러 애니메이션을 즉시 마무리해 줍니다.


@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: .01ms !important;
animation-iteration-count: 1 !important;
transition-duration: .01ms !important;
}
}


반복은 한 번으로 줄이고 시간은 눈에 안 보이게 짧혀, 상태 변화는 그대로 전하되 움직임만 뺍니다. 반응은 남기고 흔들림만 거두는 것이 타이밍을 다루는 기본 예의입니다.

85.9 정리

타이밍은 움직임의 길이이고 이징은 움직임의 성격입니다. 거리와 크기에 맞게 지속시간을 잡고, 다섯 키워드와 cubic-bezier로 결을 고르고, steps로 끊고 오버슈트로 튕기고 시차로 흐름을 엮으며, 무엇보다 눈과 손으로 느껴 보며 조율하는 것이 그 전부입니다.