UX는 화면 어딘가에 따로 떨어져 있는 감성이 아니라, 하나의 요소가 가진 여러 상태의 총합입니다. 같은 버튼이라도 기본, 마우스를 올렸을 때, 눌렀을 때, 못 누를 때, 처리 중일 때가 각각 어떻게 응답하느냐가 곧 그 화면의 경험입니다. 말로 풀면 막연하니 상태가 하나뿐인 버튼과 상태를 갖춘 버튼을 CSS로 나란히 놓고 보겠습니다.

17.1 상태가 없는 버튼

겉보기엔 멀쩡히 눌리는 버튼입니다. 그런데 손을 올려도, 눌러도, 키보드로 옮겨가도 아무 반응이 없습니다.


.btn {
background: #2563eb;
color: #fff;
border: none;
padding: 10px 16px;
border-radius: 6px;
}


동작은 하지만 사용자는 이게 눌리는 물건인지 확신하지 못합니다. 반응이 없는 화면은 늘 고장난 것처럼 느껴집니다.

17.2 hover와 active로 손에 응답하기

마우스를 올리면 색이 변하고, 누르는 순간 살짝 내려앉게 합니다. 이 두 상태가 버튼이라는 사실을 손끝에 알려 줍니다.


.btn {
background: #2563eb;
color: #fff;
padding: 10px 16px;
border-radius: 6px;
cursor: pointer;
transition: background .15s ease, transform .05s ease;
}
.btn:hover {
background: #1d4ed8;
}
.btn:active {
transform: translateY(1px);
}


커서가 손 모양으로 바뀌고, 색이 짙어지고, 누르면 1px 내려갑니다. 세 줄 남짓의 코드가 죽어 있던 사각형을 살아 있는 버튼으로 바꿉니다.

17.3 키보드 포커스를 눈에 보이게

마우스를 못 쓰는 사람은 Tab 키로 화면을 옮겨 다닙니다. 지금 초점이 어디 있는지 보이지 않으면 그 사람은 길을 잃습니다.


.btn:focus-visible {
outline: 2px solid #1d4ed8;
outline-offset: 2px;
}


여기서 :focus-visible 는 키보드로 이동했을 때만 외곽선을 그리고, 마우스 클릭에는 굳이 테두리를 남기지 않습니다. 필요한 사람에게만 정확히 신호를 보내는 셈입니다.

17.4 못 누르는 상태를 알리기

지금은 누를 수 없는 버튼이라면 그 사실이 보여야 합니다. 아무 표시 없이 클릭만 씹으면 사용자는 같은 자리를 계속 두드립니다.


.btn:disabled {
background: #93c5fd;
cursor: not-allowed;
opacity: .8;
}


흐려진 파랑과 금지 커서가 지금은 안 된다고 말해 줍니다. 폼을 다 채우기 전까지 제출 버튼을 이 상태로 두면 실수로 빈 폼을 보내는 일이 사라집니다.

17.5 처리 중 상태

제출을 눌렀는데 화면이 그대로면 사용자는 눌렸나 싶어 또 누릅니다. 처리 중임을 버튼 스스로 보여 줘야 합니다.


.btn[aria-busy="true"] {
color: transparent;
pointer-events: none;
}
.btn[aria-busy="true"]::after {
content: "";
position: absolute;
width: 16px;
height: 16px;
border: 2px solid #fff;
border-top-color: transparent;
border-radius: 50%;
animation: spin .6s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}


버튼에 aria-busy="true" 를 붙이는 순간 글자가 사라지고 그 자리에 회전하는 원이 돕니다. pointer-events: none 이 중복 클릭까지 막아 주니 서버로 같은 요청이 두 번 날아가지 않습니다.

17.6 입력 오류 상태

잘못 입력했을 때 어디가 틀렸는지 조용히 붉게 짚어 주면, 사용자는 에러 메시지를 읽기 전에 이미 알아챕니다.


.input {
border: 1px solid #d1d5db;
border-radius: 6px;
padding: 8px 10px;
}
.input:focus {
border-color: #2563eb;
outline: none;
}
.input[aria-invalid="true"] {
border-color: #dc2626;
background: #fef2f2;
}


평소엔 회색 테두리, 초점이 가면 파란 테두리, 값이 틀리면 붉은 테두리에 연한 붉은 배경까지 깔립니다. 같은 입력칸이 상황마다 다른 얼굴을 갖는 것, 그게 세심한 폼입니다.

17.7 눌리는 판을 넉넉히

보이는 크기가 작아도 실제로 눌리는 영역은 넉넉해야 합니다. 특히 손가락으로 누르는 화면에서 좁은 버튼은 옆 것을 잘못 누르게 만듭니다.


.icon-btn {
min-width: 44px;
min-height: 44px;
display: inline-flex;
align-items: center;
justify-content: center;
}


아이콘은 20px여도 눌리는 판은 44px입니다. 손끝이 조금 어긋나도 제대로 눌리니, 오작동으로 인한 짜증이 눈에 띄게 줄어듭니다.

17.8 상태를 한 컴포넌트로 모으기

지금까지 나눠 본 상태들을 하나의 버튼에 얹으면 이렇게 됩니다. 마크업은 단순한데 응답은 풍부해집니다.


<button class="btn" aria-busy="false">저장</button>

.btn {
position: relative;
background: #2563eb;
color: #fff;
padding: 10px 16px;
border: none;
border-radius: 6px;
cursor: pointer;
transition: background .15s ease;
}
.btn:hover { background: #1d4ed8; }
.btn:active { transform: translateY(1px); }
.btn:focus-visible {
outline: 2px solid #1d4ed8;
outline-offset: 2px;
}
.btn:disabled {
background: #93c5fd;
cursor: not-allowed;
}


하나의 .btn 이 기본, hover, active, focus, disabled를 모두 품고 있습니다. 사용자가 어떤 방식으로 다가오든 화면이 그에 맞게 대답하는 것, UX는 이런 대답들의 총합입니다.

17.9 선택된 상태 표시하기

탭이나 토글처럼 여러 개 중 지금 무엇이 켜져 있는지 보여야 하는 요소도 상태의 문제입니다. 선택 여부를 aria-selected 로 표시하고 그 값에 스타일을 겁니다.


.tab {
padding: 8px 14px;
color: #6b7280;
border-bottom: 2px solid transparent;
cursor: pointer;
}
.tab[aria-selected="true"] {
color: #2563eb;
border-bottom-color: #2563eb;
font-weight: 600;
}


선택된 탭만 파란 글자에 아래 밑줄이 그어지고 나머지는 흐린 회색으로 물러섭니다. 사용자는 지금 어느 화면에 있는지 굳이 헤아리지 않아도 한눈에 압니다.

17.10 정리

좋은 UX는 특별한 장식이 아니라 상태를 빠짐없이 챙기는 성실함입니다. 기본과 hover, active, focus, disabled, 처리 중, 오류까지 각 상태에 응답을 심어 두면, 사용자는 자기가 지금 무엇을 할 수 있고 무엇이 벌어지는지 늘 알 수 있습니다. 화면이 매 순간 대답해 준다는 느낌, 그 안심이 곧 경험의 질입니다.