피그마 프로토타입에서 이어 둔 움직임은, 코드에서는 상태와 전환의 규칙이 됩니다. 저는 손끝에 닿는 반응을 몇 줄의 CSS로 옮깁니다.

72.1 상태를 상호작용으로

마우스를 올렸을 때, 키보드로 짚었을 때, 눌렀을 때가 각각 다른 상태입니다.


.link {
color: #2563eb;
transition: color 0.2s ease;
}
.link:hover {
color: #1d4ed8;
}
.link:focus-visible {
outline: 2px solid #2563eb;
outline-offset: 2px;
}
.link:active {
color: #1e40af;
}


focus-visible는 키보드로 이동했을 때만 테두리를 보여 줍니다. 마우스 사용자에게는 조용하고, 키보드 사용자에게는 지금 어디를 짚었는지 또렷합니다.

72.2 transition 뜯어보기

전환은 바꿀 속성, 걸리는 시간, 움직임의 결로 이루어집니다.


.btn {
background: #2563eb;
transition-property: background;
transition-duration: 0.25s;
transition-timing-function: ease;
}


세 줄을 한 줄로 줄여 쓰면 이렇게 됩니다.


.btn {
transition: background 0.25s ease;
}


결을 더 세밀하게 다루고 싶으면 곡선을 직접 그립니다. 처음엔 느리게 시작해 끝에서 부드럽게 멈추는 결입니다.


.btn {
transition: background 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}


ease는 편안한 기본값이고, cubic-bezier는 그 곡선을 손수 조절하는 방법입니다.

72.3 transform으로 움직임

위치나 크기를 바꿀 때는 transform을 씁니다. 요소를 실제로 밀어내지 않아 주변 배치가 흔들리지 않습니다.


.card {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.card:hover {
transform: translateY(-4px) scale(1.02);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}


마우스를 올리면 카드가 살짝 떠오르며 그림자가 짙어집니다. translateY로 위로 4px 밀고, scale로 아주 조금 키운 결과입니다.

72.4 등장을 keyframes로

여러 단계를 거치는 움직임은 keyframes로 그립니다. 아래에서 스르르 떠오르며 나타나는 등장입니다.


@keyframes fade-up {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.toast {
animation: fade-up 0.3s ease both;
}


from에서 흐릿하고 아래에 있던 것이, to에서 또렷해지며 제자리로 올라옵니다. both은 시작 전과 끝난 뒤의 모습을 유지하게 합니다.

72.5 transition all은 피하기

편하다고 모든 속성을 한꺼번에 전환하면, 뜻하지 않은 값까지 딸려 움직여 버벅입니다.


.item {
transition: all 0.3s;
}


바꿀 속성만 콕 집어 적으면 움직임이 가볍고 예측 가능해집니다.


.item {
transition: transform 0.3s ease, opacity 0.3s ease;
}


무엇이 움직이는지 코드만 봐도 드러납니다. transform과 opacity는 특히 부드럽게 처리되는 속성입니다.

72.6 움직임을 줄여야 하는 사람

화면의 움직임에 어지러움을 느끼는 사용자가 있습니다. 기기 설정에서 움직임 줄이기를 켠 사람에게는 애니메이션을 걷어 냅니다.


@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}


움직임을 없애는 것이 아니라 눈 깜짝할 새로 줄여, 결과는 그대로 두되 흔들림만 거둡니다. 접근성은 움직임의 마지막 손질입니다.

72.7 눌러 여닫는 상태

프로토타입에서 버튼을 눌러 창을 여닫는 흐름은, 코드에서 상태 클래스와 전환으로 옮깁니다.


.panel {
max-height: 0;
overflow: hidden;
opacity: 0;
transition: max-height 0.3s ease, opacity 0.3s ease;
}
.panel.is-open {
max-height: 400px;
opacity: 1;
}


is-open이 붙으면 패널이 높이를 얻으며 서서히 드러납니다. 열림과 닫힘 사이를 뚝 끊지 않고 부드럽게 잇습니다.

72.8 반복되는 움직임

불러오는 동안 빙글 도는 표시처럼, 끝없이 되풀이되는 움직임도 keyframes로 그립니다.


@keyframes spin {
to {
transform: rotate(360deg);
}
}
.spinner {
width: 24px;
height: 24px;
border: 3px solid #e5e7eb;
border-top-color: #2563eb;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}


infinite가 멈추지 않게 하고, linear가 일정한 빠르기로 돌립니다. 한 바퀴를 도는 회전이라 결의 가감속 없이 고른 편이 자연스럽습니다.

72.9 시차를 둔 등장

목록의 항목이 한꺼번에 튀어나오면 어수선합니다. 조금씩 시간을 어긋내면 차례로 밀려 들어오는 결이 생깁니다.


.item {
opacity: 0;
animation: fade-up 0.4s ease forwards;
}
.item:nth-child(2) {
animation-delay: 0.08s;
}
.item:nth-child(3) {
animation-delay: 0.16s;
}


animation-delay로 각 항목의 시작을 조금씩 늦춥니다. forwards는 끝난 뒤의 모습을 붙들어, 다 나타난 뒤 흐릿하게 돌아가지 않게 합니다.

72.10 눌리는 느낌 주기

버튼을 눌렀을 때 살짝 들어가는 느낌은, 사용자에게 네 손짓이 닿았다고 조용히 알려 줍니다.


.press {
transition: transform 0.1s ease;
}
.press:active {
transform: scale(0.96);
}


누르는 동안만 조금 작아졌다가 손을 떼면 돌아옵니다. 아주 짧고 작은 움직임이지만, 이 한 번의 반응이 화면을 살아 있게 합니다.

72.11 손끝에 닿는 반응

상태는 가상 클래스로, 변화는 transition으로, 여러 단계의 움직임은 keyframes로 옮기면, 프로토타입에서 이어 둔 흐름이 실제 손끝의 반응이 됩니다.


.btn:hover {
transform: translateY(-1px);
}


바꿀 속성만 고르고, 움직임을 힘겨워하는 사람까지 헤아리면, 절제된 움직임이 화면을 품격 있게 합니다.