사용자가 뭔가를 눌렀는데 화면이 조용하면, 됐는지 안 됐는지 몰라 같은 동작을 반복합니다. 이번에는 성공, 실패, 로딩, 빈 화면 같은 상태를 눈에 보이게 만드는 CSS를 코드로 정리하겠습니다. 아래 예제는 그대로 붙여 확인할 수 있습니다.

15.1 성공과 실패를 색으로 알리기

결과 메시지는 글자만으로는 약합니다. 색과 배경을 함께 줘야 성공인지 오류인지 순간에 읽힙니다.


.toast {
padding: 12px 16px;
border-radius: 8px;
font-size: 14px;
border-left: 4px solid;
}
.toast-success {
background: #ecfdf5;
color: #065f46;
border-color: #10b981;
}
.toast-error {
background: #fef2f2;
color: #991b1b;
border-color: #ef4444;
}


왼쪽 색 띠가 성격을 먼저 알리고 옅은 배경이 메시지를 감쌉니다. 초록과 빨강이 읽기 전에 결과를 짐작하게 해 줍니다.

15.2 로딩 스피너 돌리기

데이터를 기다리는 동안 빈 화면만 있으면 멈춘 것처럼 보입니다. 도는 원 하나로 지금 일하는 중임을 알립니다.


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


회색 테두리에 파란 한 조각만 돌아 움직임을 만듭니다. 무한 회전이라 응답이 늦어도 화면이 살아 있는 느낌을 유지합니다.

15.3 뼈대 화면으로 기다림 채우기

목록을 불러오는 동안 회색 뼈대를 보여 주면 곧 내용이 온다는 예고가 됩니다. 스피너보다 덜 답답합니다.


.skeleton {
height: 14px;
border-radius: 4px;
background: linear-gradient(90deg, #eee 25%, #f5f5f5 37%, #eee 63%);
background-size: 400% 100%;
animation: shimmer 1.4s infinite;
}
@keyframes shimmer {
to { background-position: -100% 0; }
}


옅은 빛이 좌우로 흐르며 곧 채워질 자리를 미리 그려 줍니다. 실제 내용과 크기가 같아 로딩이 끝나도 화면이 튀지 않습니다.

15.4 진행 막대로 남은 양 보여 주기

업로드처럼 시간이 걸리는 일은 얼마나 남았는지 보여 줘야 기다릴 마음이 생깁니다.


.progress {
height: 8px;
background: #e5e7eb;
border-radius: 999px;
overflow: hidden;
}
.progress-bar {
height: 100%;
width: 60%;
background: #2563eb;
transition: width .3s ease;
}


파란 막대가 폭으로 진행률을 나타내고, transition 덕에 값이 바뀔 때 매끄럽게 늘어납니다. overflow: hidden이 둥근 모서리 밖으로 색이 새는 것을 막습니다.

15.5 입력 오류를 필드 옆에 붙이기

어디가 틀렸는지 화면 맨 위 한 줄로 뭉뚱그리면 사용자가 찾아 헤맵니다. 문제의 칸 바로 옆에서 짚어 줍니다.


.input.is-invalid {
border-color: #ef4444;
background: #fef2f2;
}
.field-error {
color: #b91c1c;
font-size: 13px;
margin-top: 4px;
}


틀린 칸의 테두리가 붉게 변하고 바로 아래 짧은 설명이 붙습니다. 시선이 문제와 원인을 한 번에 잡습니다.

15.6 빈 화면을 안내로 바꾸기

아직 데이터가 없을 때 텅 빈 화면은 고장처럼 보입니다. 왜 비었는지와 다음 행동을 담아 두면 막다른 길이 되지 않습니다.


.empty {
text-align: center;
padding: 48px 20px;
color: #6b7280;
}
.empty h3 {
color: #374151;
margin-bottom: 6px;
}


가운데 정렬한 안내가 아직 글이 없어요 같은 이유를 말하고, 그 아래 버튼으로 첫 행동을 권합니다. 빈 화면이 초대장으로 바뀝니다.

15.7 잠깐 나타났다 사라지는 알림

토스트는 스르르 나타나 잠시 머물다 사라져야 자연스럽습니다. 갑자기 튀어나오면 놀랍니다.


.toast {
opacity: 0;
transform: translateY(8px);
transition: opacity .25s, transform .25s;
}
.toast.show {
opacity: 1;
transform: translateY(0);
}


show 클래스가 붙는 순간 아래에서 떠오르며 서서히 진해집니다. 사라질 때 클래스를 떼면 같은 길로 부드럽게 물러납니다.

15.8 상태 배지로 한눈에 구분하기

목록에서 각 항목의 상태를 글자로만 적으면 잘 안 읽힙니다. 작은 배지로 색을 입혀 훑어보기 쉽게 만듭니다.


.badge {
display: inline-block;
padding: 2px 8px;
border-radius: 999px;
font-size: 12px;
font-weight: 600;
}
.badge-done {
background: #dcfce7;
color: #166534;
}
.badge-wait {
background: #fef9c3;
color: #854d0e;
}


둥근 알약 모양에 상태별 색을 넣어 완료와 대기가 눈에 바로 갈립니다. 목록을 위아래로 훑기만 해도 흐름이 읽힙니다.

15.9 움직임을 줄여 달라는 요청 존중하기

회전이나 반짝임이 어지러운 사용자도 있습니다. 시스템 설정을 읽어 애니메이션을 줄여 줍니다.


@media (prefers-reduced-motion: reduce) {
.spinner,
.skeleton {
animation: none;
}
}


움직임을 줄이겠다고 설정한 기기에서는 회전과 반짝임이 멈춥니다. 상태 자체는 색과 형태로 남아 정보가 사라지지는 않습니다.

15.10 제출 중 중복 클릭 막기

저장 요청이 오가는 사이에 버튼을 여러 번 누르면 같은 요청이 겹쳐 나갑니다. 처리 중에는 버튼을 잠가 둡니다.


.submit[disabled] {
opacity: .6;
cursor: progress;
pointer-events: none;
}


disabled가 걸리면 흐려지고 progress 커서가 일하는 중임을 알립니다. pointer-events: none이 그 사이 클릭을 아예 무시해 중복 전송을 막습니다.

15.11 정리

피드백은 성공과 실패를 색으로 나누고, 기다림은 스피너나 뼈대로 채우고, 오류는 문제의 칸 옆에서 짚어 주는 것이 핵심입니다. 빈 화면조차 다음 행동으로 안내하면 사용자는 길을 잃지 않습니다. 화면이 계속 답을 해 주면 사용자는 안심하고 다음을 누릅니다.