인터랙션은 부르면(트리거) 답하는(피드백) 한 쌍입니다. 어느 한쪽만 어긋나도 대화가 삐끗하지요. 트리거 종류마다 어떤 피드백을 짝지어야 하는지, CSS로 하나씩 짜 보겠습니다.
77.1 호버: 부름과 응답이 붙어 있는 자리
마우스를 요소 위에 올리는 것 자체가 트리거이고, 그 순간 살짝 떠오르는 것이 곧 피드백입니다. 여기는 만질 수 있는 자리라고 미리 귀띔하는 셈이지요.
.card {
background: #fff;
border-radius: 10px;
box-shadow: 0 1px 3px rgba(0, 0, 0, .12);
transition: box-shadow .2s ease, transform .2s ease;
}
.card:hover {
box-shadow: 0 8px 20px rgba(0, 0, 0, .16);
transform: translateY(-4px);
}
손을 올리면 그림자가 짙어지며 카드가 슬쩍 떠오릅니다. 이 작은 응답 하나가 여기를 누르면 무언가 일어난다고 손끝에 일러 줍니다.
77.2 클릭과 눌림: :active로 즉각 답하기
피드백에서 가장 먼저 지킬 약속은 즉각성입니다. 손끝이 닿는 그 찰나에 버튼이 살짝 안으로 들어가면, 사용자는 명령이 받아들여졌다고 안심하지요.
.btn {
background: #2563eb;
color: #fff;
padding: 10px 18px;
border: none;
border-radius: 8px;
cursor: pointer;
transition: background .15s ease, transform .08s ease;
}
.btn:hover {
background: #1d4ed8;
}
.btn:active {
transform: translateY(1px) scale(.98);
}
호버로 색이 짙어지고, 누르는 순간 1px 내려앉으며 아주 살짝 작아집니다. 결과가 나오기까지 시간이 걸리더라도 이 첫 신호만은 손이 떨어지기 전에 돌려주는 것이 핵심입니다.
77.3 키보드의 길잡이: :focus-visible
마우스 없이 탭 키로 화면을 오가는 사용자에게는, 지금 어디에 머물러 있는지 알리는 또렷한 테두리가 유일한 이정표입니다. 이 표시는 지우는 것이 아니라 잘 보이게 가꾸는 것이 옳습니다.
.btn:focus-visible {
outline: 3px solid #93c5fd;
outline-offset: 2px;
}
.btn:focus:not(:focus-visible) {
outline: none;
}
:focus-visible는 키보드로 이동해 왔을 때만 링을 보여 주고, 마우스로 누른 경우에는 조용히 감춥니다. 키보드 사용자에게는 나침반을 쥐여 주고, 마우스 사용자에게는 거슬리는 테두리를 남기지 않는 영리한 짝이지요.
77.4 입력칸이 깨어나는 순간: :focus
입력 칸에 커서를 두는 것도 트리거입니다. 여기에는 손을 떼기 전까지 머무는 피드백이 어울리지요. 지금 어느 칸에 글자가 들어가는지 한눈에 잡히게 만듭니다.
.input {
border: 1px solid #d1d5db;
border-radius: 8px;
padding: 10px 12px;
transition: border-color .15s ease, box-shadow .15s ease;
}
.input:focus {
outline: none;
border-color: #2563eb;
box-shadow: 0 0 0 3px rgba(37, 99, 235, .18);
}
클릭해 커서를 두면 테두리가 파래지고 은은한 링이 감쌉니다. 이 반응은 순간 반짝이고 사라지지 않고 커서가 머무는 내내 유지되기에, 입력 중이라는 사실이 계속 눈에 남습니다.
같은 방식으로 잘못된 값이 들어왔을 때는 붉은 결을 입혀 알립니다. 색이 유일한 신호가 되지 않도록 테두리와 링을 함께 씁니다.
.input:user-invalid {
border-color: #ef4444;
box-shadow: 0 0 0 3px rgba(239, 68, 68, .18);
}
:user-invalid는 사용자가 실제로 그 칸을 만지고 떠난 뒤에만 붉게 물듭니다. 아직 손대지도 않은 빈 칸을 미리 틀렸다고 다그치지 않으니, 응답이 사용자의 행동을 앞지르지 않습니다.
77.5 로드 트리거: 스르르 나타나는 요소
트리거를 사람만 당기는 것은 아닙니다. 화면이 뜨는 순간이나 요소가 등장하는 시점처럼 시스템이 스스로 당기는 트리거도 있지요. 불쑥 튀어나오는 대신 부드럽게 스며들게 합니다.
@keyframes fade-slide-in {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.appear {
animation: fade-slide-in .4s ease-out both;
}
요소가 붙는 순간 아래에서 위로 12px 올라오며 서서히 또렷해집니다. both를 두면 시작 전과 끝난 뒤의 상태가 모두 유지되어, 애니메이션 앞뒤로 요소가 깜빡이지 않습니다.
여러 요소를 차례로 등장시키면 리듬이 생깁니다. 각 항목의 시작을 조금씩 늦춰 계단처럼 흐르게 해 봅니다.
.list li {
animation: fade-slide-in .4s ease-out both;
}
.list li:nth-child(2) {
animation-delay: .08s;
}
.list li:nth-child(3) {
animation-delay: .16s;
}
.list li:nth-child(4) {
animation-delay: .24s;
}
항목들이 한꺼번에 쏟아지지 않고 물결처럼 순서대로 들어옵니다. 이 짧은 시차가 목록이 살아 움직이며 자리를 잡는 인상을 만듭니다.
77.6 커서와 잠금: 손대기 전에 알리는 피드백
커서의 모양도 조용한 피드백입니다. 누를 수 있는 곳에서는 손 모양으로, 지금은 손댈 수 없는 곳에서는 금지 표시로 바뀌며, 커서는 말없이 여기서 무엇을 할 수 있는지 일러 주지요.
.btn {
cursor: pointer;
}
.btn:disabled {
background: #93c5fd;
color: #eff6ff;
cursor: not-allowed;
transform: none;
}
흐려진 파랑과 금지 커서가 지금은 못 누른다고 손대기 전에 알려 줍니다. 잠긴 버튼은 :active의 눌림 반응도 transform: none으로 눌러 두어, 눌러도 아무 일이 없다는 사실까지 정직하게 전합니다.
77.7 움직임을 원하지 않는 사용자 존중하기
어떤 사용자는 화면이 움직이면 어지럼을 느낍니다. 트리거와 피드백을 얹을 때는 처음부터 이 사람들을 위한 스위치를 함께 답니다.
@media (prefers-reduced-motion: reduce) {
.card,
.btn,
.input {
transition: none;
}
.appear,
.list li {
animation: none;
}
}
운영체제에서 움직임 줄이기를 켠 사람에게는 전환과 등장 애니메이션이 즉시 사라지고 상태만 바뀝니다. 응답 자체는 그대로 전하되 움직임만 걷어 낸 것, 이것이 피드백을 다루는 기본 예의입니다.
77.8 정리
트리거와 피드백은 부르면 답하는 한 쌍입니다. 호버로 만질 수 있음을 귀띔하고, 누름에는 즉각 답하고, 키보드에는 또렷한 길잡이를 주고, 입력에는 머무는 신호를 남기고, 등장에는 부드러운 결을 입히는 것. 방아쇠를 하나 심을 때마다 짝지을 응답을 함께 그리면, 화면은 한결 살아 있는 것처럼 느껴집니다.