블로그 목록
frontend

CSS :has() 선택자 — 드디어 부모를 선택할 수 있게 됐다

폼을 만들다 보면 늘 같은 데서 막혔다. input에 에러가 있을 때 그 input을 감싸는 부모 div에 빨간 테두리를 주고 싶은데, CSS로는 부모를 선택할 방법이 없었다. 결국 JavaScript로 클래스를 토글하거나, 상태를 끌어올려서 부모에 조건부 클래스를 붙이는 식으로 처리했다. 별것 아닌 스타일 하나 때문에 로직이 늘어나는 게 늘 거슬렸다.

:has()가 이걸 그냥 끝내준다. CSS만으로 "특정 자식을 가진 부모"를 선택할 수 있다. 흔히 부모 선택자라고 부르는데, 사실 부모뿐 아니라 형제 관계까지 잡을 수 있어서 훨씬 강력하다.

기본 개념

A:has(B)는 "B를 안에 가지고 있는 A"를 선택한다. 기준이 되는 건 어디까지나 A다.

/* 체크된 체크박스를 가진 label */
label:has(input:checked) {
  font-weight: bold;
  color: #2563eb;
}

/* 이미지가 들어있는 카드만 패딩 제거 */
.card:has(img) {
  padding: 0;
}

label:has(input:checked)에서 실제로 스타일이 적용되는 건 input이 아니라 label이다. 자식 상태를 보고 부모를 꾸미는 것, 이게 핵심이다.

폼 검증에 써보기

처음에 말한 그 문제. 이제 JavaScript 없이 된다.

/* 유효하지 않은 input을 감싼 필드 그룹에 에러 스타일 */
.field:has(input:invalid) {
  border-left: 3px solid #ef4444;
  background: #fef2f2;
}

/* 비어있지 않고 유효한 경우 */
.field:has(input:valid:not(:placeholder-shown)) {
  border-left: 3px solid #22c55e;
}

:invalid, :placeholder-shown 같은 의사 클래스랑 조합하면 상태 표시가 전부 CSS 안에서 끝난다. React 상태 하나 줄었다.

형제 선택도 된다

:has()는 결합자랑 같이 쓸 수 있어서 활용 범위가 넓다.

/* 바로 뒤에 에러 메시지가 따라오는 input */
input:has(+ .error-message) {
  border-color: #ef4444;
}

/* 자식 중에 열린 details가 있으면 컨테이너 배경 변경 */
.accordion:has(details[open]) {
  background: #f8fafc;
}

input:has(+ .error-message)는 "바로 다음 형제로 .error-message를 둔 input"을 잡는다. 마크업 구조를 그대로 두고 스타일만 반응시킬 수 있다.

한 가지 주의점

:has() 안의 선택자는 특정도(specificity) 계산에 포함된다. 인자 중 가장 강한 선택자가 기준이 되니까, 무심코 ID를 넣으면 특정도가 확 올라가서 다른 규칙을 덮어쓸 수 있다. 디버깅이 까다로워지므로 가급적 클래스나 의사 클래스 위주로 쓰는 게 안전하다.

지원 범위는 이제 걱정 안 해도 된다. 2023년 말부터 모든 주요 브라우저가 지원하고, 2026년 현재는 사실상 안 되는 환경을 찾기가 더 어렵다.


:has()를 알고 나니 그동안 JavaScript로 처리하던 스타일 토글이 꽤 많이 사라졌다. 자식 상태에 따라 부모를 바꾸고 싶을 때, 클래스 토글 로직을 짜기 전에 :has()로 되는지부터 한 번 떠올려보면 코드가 가벼워진다.