블로그 목록
frontend

CSS :user-valid / :user-invalid로 폼 검증 UX 개선하기 — 입력도 안 했는데 빨간불 그만

폼 검증 스타일을 :invalid로 붙여본 적 있으면 이 짜증을 안다. 이메일 입력창에 required를 걸고 input:invalid { border-color: red; }를 주면, 사용자가 아무것도 입력하지 않은 상태 — 즉 페이지가 막 뜬 순간부터 모든 칸이 빨갛게 물든다. 아직 입력할 기회도 안 줬는데 "너 틀렸어"라고 소리치는 셈이다.

그래서 다들 JS로 처리했다. blur 이벤트를 걸고, "touched" 상태를 따로 관리하고, 클래스를 토글하고... 폼 하나에 상태 관리 코드가 붙는다.

:user-invalid이 하는 일

:user-invalid는 "사용자가 실제로 상호작용한 뒤에" 값이 유효하지 않을 때만 매칭된다. 정확히는 사용자가 값을 입력하거나 폼을 제출하려 시도한 다음부터 판정이 켜진다. 반대로 :user-valid는 상호작용 후 유효할 때 매칭된다.

:invalid와 규칙 자체는 같다. 차이는 타이밍이다. :invalid는 처음부터 판정하고, :user-invalid는 사용자가 만진 뒤에 판정한다. 우리가 JS로 힘겹게 흉내 내던 "touched" 로직이 브라우저에 기본 내장된 것이다.

써보기

<label>
  이메일
  <input type="email" required />
  <span class="hint">올바른 이메일 형식이 아닙니다</span>
</label>
input:user-invalid {
  border-color: #e5484d;
}
input:user-valid {
  border-color: #30a46c;
}

/* 에러 메시지는 평소 숨기고, 잘못 입력했을 때만 노출 */
.hint {
  display: none;
  color: #e5484d;
  font-size: 0.85rem;
}
input:user-invalid + .hint {
  display: block;
}

이게 전부다. 페이지 로드 직후엔 아무 표시도 없다가, 사용자가 이메일 칸에 abc까지만 치고 다른 곳으로 넘어가면 그때 빨간 테두리와 힌트가 뜬다. 제대로 된 이메일을 넣으면 초록으로 바뀐다. JS는 한 줄도 없다.

:has()랑 붙이면 더 깔끔하다

에러 메시지 대신 label 전체 톤을 바꾸고 싶다면 부모를 선택해야 한다. :has()와 조합하면 된다.

label:has(input:user-invalid) {
  color: #e5484d;
}

한 가지 주의점

:user-invalid는 요소가 포커스를 잃거나(blur) 폼 제출을 시도하기 전까지는 켜지지 않는다. 그래서 "입력하는 도중에는 조용하다가, 다 치고 칸을 벗어나면 검사"라는, 사실 우리가 원하던 바로 그 UX가 공짜로 나온다. 반대로 실시간 검증이 꼭 필요하면 이 선택자만으로는 부족하니 그땐 JS를 써야 한다.

지원 범위도 넉넉하다. 2023년 후반부터 주요 브라우저에 다 들어왔고 지금은 Baseline이다. 폼 검증 UX 하나 만들자고 상태 훅 붙이던 시절은 지났다 — 선택자 두 개면 된다.