본문으로 건너뛰기

CSS :has() 선택자 가이드: 부모 요소 선택이 드디어 가능해졌다

CSS22분 읽기본문 11,050

핵심 요약

이 글에서 확인할 내용

CSS 역사상 가장 오랫동안 개발자들이 염원해왔던 기능, 바로 "부모 선택자"입니다. jQuery의 .parent() 나 .closest() 를 쓰지 않고는 불가능했던 그 스타일링이, 드디어 순수 CSS만으로 가능해졌습니다. 주인공은 바로 :has() 의사 클래스(Pseudo-class)입니다. Safari 15.

  • 1. :has()란 무엇인가? — 관계 선택자의 등장
  • 2. 특이성(Specificity)은 어떻게 계산되는가
  • 3. 실전 활용 패턴 BEST 3

CSS :has() 선택자로 부모 요소를 선택하는 개념을 시각화한 다이어그램

CSS 역사상 가장 오랫동안 개발자들이 염원해왔던 기능, 바로 "부모 선택자"입니다. jQuery의 .parent().closest()를 쓰지 않고는 불가능했던 그 스타일링이, 드디어 순수 CSS만으로 가능해졌습니다. 주인공은 바로 :has() 의사 클래스(Pseudo-class)입니다. Safari 15.4(2022년 3월)를 시작으로 Chrome·Edge 105(2022년), Firefox 121(2023년 12월)이 뒤를 이으면서, 2024년부터는 모든 메이저 브라우저에서 별도 플래그 없이 쓸 수 있게 되었습니다. 이 선택자를 기초 문법부터 특이성 계산, 실전 패턴, 성능 주의점, 그리고 브라우저 지원과 폴백까지 실무 관점에서 차례로 정리합니다.

우리가 이 글에서 붙들고 갈 질문은 하나입니다. "지금까지 JavaScript로만 표현할 수 있었던 UI 상태를, :has()는 어디까지 선언적인 CSS로 대체할 수 있는가, 그리고 그 대가는 무엇인가." 마법처럼 보이는 기능일수록 내부 동작과 비용을 알고 써야 실무에서 사고가 나지 않습니다.

1. :has()란 무엇인가? — 관계 선택자의 등장

MDN 정의에 따르면 :has()는 관계형(relational) 의사 클래스입니다. 쉽게 말해 "인수(괄호 안의 상대 선택자)와 일치하는 요소를 하나라도 포함하고 있는 요소"를 선택합니다.

/* 자식 중에 img 태그가 있는 article 요소만 선택 */
article:has(img) {
  background-color: #f0f9ff;
  border: 1px solid #bae6fd;
}

이 코드는 img를 가진 article의 배경색을 바꿉니다. 만약 img가 없다면 스타일은 적용되지 않습니다. 즉, 자식의 상태에 따라 부모의 스타일을 결정할 수 있게 된 것입니다.

왜 지금까지는 부모를 선택할 수 없었나

이 기능이 "드디어" 나왔다는 표현이 과장이 아닌 이유를 이해하려면, 브라우저가 CSS를 매칭하는 방식을 잠깐 들여다봐야 합니다. 전통적인 CSS의 철학은 이름 그대로 캐스케이드(Cascade), 즉 하향식이었습니다. 브라우저는 HTML을 스트리밍으로 파싱하면서 위에서 아래로, 바깥에서 안으로 요소를 만납니다. 이때 셀렉터 매칭은 대개 오른쪽에서 왼쪽으로(key selector부터) 평가되지만, 결정적으로 어떤 요소의 스타일은 그 요소보다 앞서 파싱된 정보만으로 확정될 수 있어야 렌더링을 멈추지 않고 진행할 수 있었습니다.

부모 선택자는 이 전제를 정면으로 위반합니다. "자식 안에 특정 요소가 있으면 부모의 스타일을 바꿔라"는 규칙은, 부모를 이미 그리기 시작한 뒤에 자식을 보고 나서야 부모의 스타일이 결정된다는 뜻입니다. 과거 브라우저 엔진에게 이는 렌더링 파이프라인을 거꾸로 되감는 값비싼 요구였고, 그래서 CSSWG는 오랫동안 "부모 선택자는 성능상 위험하다"는 이유로 표준화를 미뤄왔습니다. :has()가 이제야 등장한 것은 개념이 없어서가 아니라, 엔진이 이 역방향 매칭을 감당할 수 있을 만큼 스타일 무효화(invalidation) 최적화가 성숙했기 때문입니다.

앵커 요소와 상대 선택자 리스트

:has()의 문법을 정확히 읽는 열쇠는 앵커(anchor) 요소라는 개념입니다. A:has(B)에서 스타일이 적용되는 대상은 어디까지나 A이고, 괄호 안의 BA를 기준점으로 삼아 상대적으로 평가되는 상대 선택자(relative selector)입니다. 괄호 안에서 결합자(combinator)를 어떻게 쓰느냐에 따라 탐색 범위가 완전히 달라집니다.

article:has(img)는 후손 어딘가에 img가 있으면 매칭되지만, article:has(> img)직계 자식으로 img가 있을 때만 매칭됩니다. h1:has(+ p)는 바로 다음 형제가 ph1을, .label:has(~ .error)는 뒤따르는 형제 중 .error가 있는 .label을 선택합니다. 즉 :has()는 단순히 "부모 선택자"가 아니라, 자기 자신을 기준으로 후손·다음 형제·이전 형제까지 아우르는 범용 관계 선택자입니다. 이 점을 놓치면 "왜 내 :has()가 예상보다 많은 요소를 잡지?" 같은 혼란에 빠지기 쉽습니다. 괄호 안에서 결합자를 명시해 탐색 범위를 좁히는 습관은 정확성뿐 아니라 뒤에서 다룰 성능에도 직결됩니다.

2. 특이성(Specificity)은 어떻게 계산되는가

:has()를 실무에 도입하면 반드시 부딪히는 것이 특이성 문제입니다. MDN에 따르면 :has()의 특이성은 인수 안에서 가장 특이성이 높은 선택자의 값을 그대로 가져옵니다. 이는 :is(), :not()과 동일한 규칙입니다. 즉 :has()라는 껍데기 자체는 특이성을 더하지 않고, 괄호 안 내용물 중 최댓값이 앵커 요소의 특이성에 합산됩니다.

예를 들어 article:has(.badge)의 특이성은 "요소 하나(article) + 클래스 하나(.badge)"입니다. 그런데 article:has(#promo)처럼 괄호 안에 ID 선택자를 넣으면, ID 하나의 무게가 통째로 딸려 들어와 예상보다 훨씬 강한 규칙이 되어버립니다. 디자인 시스템에서 이런 규칙이 몇 개만 섞여도 "왜 이 스타일이 다른 규칙을 이기는지" 추적하기가 어려워집니다. 특이성 전쟁을 구조적으로 다스리는 방법은 CSS @layer로 캐스케이드 레이어를 설계하는 접근과 함께 보면 이해가 빠릅니다.

특이성을 의도적으로 죽이고 싶다면 :where()를 활용합니다. :where()는 항상 특이성이 0이므로, article:has(:where(#promo))처럼 감싸면 앵커인 article의 특이성만 남고 괄호 안은 매칭 조건으로만 작동합니다. 반대로 특이성을 확실히 확보하고 싶을 때만 :is()로 감싸는 식으로, 우리는 :has() 규칙의 무게를 세밀하게 조율할 수 있습니다. 이 조율을 무시하면 나중에 !important로 급하게 덮는 악순환이 시작됩니다.

3. 실전 활용 패턴 BEST 3

3.1 "카드가 이미지를 가질 때만" 레이아웃 변경하기

블로그 카드 컴포넌트를 만들 때, 썸네일 이미지가 있는 경우와 없는 경우의 레이아웃을 다르게 하고 싶을 때가 많습니다. 예전에는 React에서 hasImage 같은 클래스를 조건부로 붙여줬어야 했습니다.

.card {
  display: grid;
  grid-template-columns: 1fr; /* 기본: 1단 */
}

/* 이미지를 포함하고 있다면 2단 컬럼으로 변경 */
.card:has(.card-image) {
  grid-template-columns: 1fr 1fr;
}

JS 로직 없이 CSS만으로 깔끔하게 해결됩니다. 여기서 중요한 실무적 이점은, 마크업이 데이터의 진실을 이미 담고 있다는 점입니다. 서버가 이미지가 있는 카드에는 .card-image를 렌더링하고 없으면 생략하는 순간, 레이아웃 분기는 이미 DOM에 존재하는 사실입니다. 별도로 hasImage 불리언을 계산해 클래스를 토글하는 코드는 이 사실을 한 번 더 복제하는 것이고, 복제된 상태는 언젠가 어긋납니다. :has()는 그 이중 관리를 없애줍니다.

3.2 폼 유효성 검사 스타일링 (Form Validation)

입력 필드에 에러가 있을 때, 그 input 뿐만 아니라 이를 감싸고 있는 라벨이나 컨테이너 그룹 전체의 색상을 빨간색으로 바꾸고 싶다면 어떨까요?

/* input이 invalid 상태라면, 그 부모인 .input-group의 테두리를 빨갛게 */
.input-group:has(input:invalid) {
  border-color: red;
}

/* 에러 메시지(p.error)가 존재한다면 라벨 색상 변경 */
label:has(+ input + p.error) {
  color: red;
}

복잡한 JS 상태 관리 없이도 폼의 시각적 피드백을 풍부하게 만들 수 있습니다. 특히 input:invalid, :required, :user-invalid 같은 표준 폼 상태 의사 클래스와 결합할 때 진가가 드러납니다. input:invalid는 페이지 로드 직후부터 빈 필수 입력을 무효로 판정해 성급하게 붉어질 수 있어 사용자에게 공격적으로 느껴지는데, 사용자가 실제로 필드를 건드린 뒤에만 무효를 표시하는 :user-invalid:has()를 함께 쓰면 "건드리기 전에는 조용하고, 잘못 입력한 뒤에야 경고하는" 자연스러운 폼을 CSS만으로 만들 수 있습니다. 제출 버튼을 폼 전체 유효성에 묶어 form:has(input:invalid) button[type="submit"] { opacity: .5; pointer-events: none; }처럼 비활성 상태를 표현하는 것도 흔한 실전 패턴입니다.

3.3 이전 형제 선택하기 (Previous Sibling)

CSS에는 '다음 형제(+)'나 '일반 형제(~)' 선택자는 있었지만, '이전 형제'를 선택하는 방법은 없었습니다. :has()를 응용하면 이것이 가능해집니다.

/* 마우스가 호버된 아이템의 '앞에 있는' 아이템 선택 */
.item:has(+ .item:hover) {
  transform: scale(0.95);
  opacity: 0.7;
}

애플의 독(Dock) 메뉴처럼 마우스를 올린 아이템 주변이 반응하는 인터랙션을 CSS만으로 구현할 수 있습니다. 여기서 눈여겨볼 것은, :has()가 상태 의사 클래스(:hover, :focus-within, :checked)와 만나면 정적인 구조 선택을 넘어 동적인 상호작용 선택으로 확장된다는 점입니다. 내비게이션에서 .nav-item:has(> .submenu:hover)로 하위 메뉴가 열린 항목을 강조하거나, nav:has(:focus-visible)로 키보드 포커스가 내부에 들어온 순간 전체 내비게이션 배경을 바꾸는 식의 접근성 친화적 표현도 가능합니다. 과거라면 mouseenter/focusin 이벤트 리스너와 상태 변수가 필요했던 일들입니다.

4. 논리 조합: :not()과 함께 쓰기

:has():not()과 결합했을 때 더욱 강력해집니다.

/* h2 태그를 포함하지 '않은' 섹션만 선택 */
section:not(:has(h2)) {
  border: 1px dashed gray; /* 제목 없는 섹션 강조 표시 */
}

이처럼 특정 요소가 '결여된' 상태를 스타일링하는 것은 기존에는 불가능에 가까웠던 작업입니다. :not(:has(...)) 조합은 "무엇이 있는가"가 아니라 "무엇이 빠졌는가"를 기준으로 삼는다는 점에서 사고방식을 뒤집습니다. 접근성 린팅을 CSS로 시각화하는 용도로도 유용합니다. 예를 들어 a:not(:has(*)):empty로 텍스트 없는 빈 링크를 개발 중 눈에 띄게 만들거나, figure:not(:has(figcaption))로 캡션이 누락된 그림을 표시해 마크업 결함을 조기에 발견할 수 있습니다. 다만 뒤에서 다루겠지만 :not() 안에 :has()를 넣는 것은 되지만, :has() 안에 다시 :has()를 넣는 것은 금지되어 있으니 구별해야 합니다.

5. 성능 이슈는 없을까?

:has()는 브라우저 렌더링 엔진 입장에서 꽤 비싼 연산입니다. 요소를 렌더링하다가 자식 요소를 확인하기 위해 다시 DOM 트리를 확인해야 하기 때문입니다. 하지만 최신 브라우저 엔진들은 고도로 최적화되어 있어, 일반적인 웹 페이지 규모에서는 성능 저하를 거의 체감할 수 없습니다. 다만 수천 개의 요소가 있는 거대한 리스트에서 복잡한 :has() 연쇄를 사용하는 것은 피하는 것이 좋습니다.

조금 더 정확히 말하면, 비용의 핵심은 최초 매칭보다 스타일 무효화(style invalidation)에 있습니다. 브라우저는 어떤 DOM 변경이나 상태 변화가 일어났을 때, 그 변화가 어떤 요소들의 스타일을 다시 계산하게 만드는지를 "무효화 세트(invalidation set)"로 추적합니다. 일반적인 하향식 선택자는 변경된 요소와 그 후손만 다시 보면 되지만, :has()는 방향이 반대라 자식이나 뒤 형제의 변화가 앞쪽/위쪽 앵커의 스타일을 되돌아보게 만듭니다. 엔진은 이를 감당하기 위해 정교한 최적화를 넣어두었지만, 우리가 규칙을 어떻게 쓰느냐에 따라 무효화 범위가 크게 달라집니다.

실무 지침은 세 가지로 요약됩니다. 첫째, 앵커를 좁혀라. body:has(...):root:has(...)처럼 전역에 앵커를 두면, 하위 어디서든 조건이 바뀔 때마다 문서 상단이 재평가 후보에 들어갑니다. 가능하면 .form-field:has(...)처럼 컴포넌트 경계 안으로 앵커를 제한하세요. 둘째, 괄호 안에서 결합자로 탐색을 좁혀라. :has(.error)(후손 전체 탐색)보다 :has(> .error):has(+ .error)가 엔진이 확인할 후보를 줄여줍니다. 셋째, 고빈도로 바뀌는 상태를 넓은 앵커에 묶지 마라. 거대한 트리 전체를 앵커로 두고 그 안의 :hover 변화에 반응시키면 포인터가 움직일 때마다 무효화가 유발될 수 있습니다.

운영 관점에서 한 번 더 — CSS-in-JS의 종말?

:has(), 컨테이너 쿼리(@container), 중첩(Nesting) 등 최근 CSS의 발전 속도는 눈에 띄게 빨라졌습니다. 과거 JS로만 처리해야 했던 많은 로직들이 순수 CSS의 영역으로 넘어오고 있습니다. 빌드 도구 관점의 변화까지 함께 보고 싶다면 Tailwind CSS v4의 CSS-first 엔진 전환 같은 흐름도 같은 맥락에 있습니다.

물론 동적인 상태 관리가 필요한 부분은 여전히 JS가 필요하겠지만, "스타일은 CSS에게, 로직은 JS에게"라는 본연의 역할 분담이 더욱 명확해지고 있습니다. 여기서 우리가 강조하고 싶은 실무적 균형점은, :has()가 "JS를 없애는 도구"가 아니라 "뷰 상태(view state)를 마크업의 진실로 되돌리는 도구"라는 것입니다. 열림/닫힘, 유효/무효, 선택됨/해제됨 같은 상태는 원래 DOM에 이미 표현되어 있는 경우가 많습니다. 그 사실을 별도의 JS 상태로 복제하지 않고 CSS가 직접 읽게 하면, 상태 동기화 버그의 상당수가 애초에 발생할 수 없게 됩니다. 지금 당장 :has()를 써보세요. 제이쿼리의 추억과 함께, 새 세상이 열릴 것입니다.

본문 중간 핵심 흐름을 시각화한 CSS 일러스트

6. 실무 활용 패턴: :has()로 해결하는 추가 문제들

6.1 빈 상태(Empty State) 처리

리스트가 비어있을 때 "데이터가 없습니다"와 같은 빈 상태 메시지를 보여주는 것도 :has()로 우아하게 해결됩니다. .list-container:not(:has(.list-item)) 형태로 JavaScript 없이 빈 상태를 감지하고 적절한 UI를 표시할 수 있습니다. 실무에서는 빈 상태 메시지를 항상 마크업에 심어두고 기본적으로 display: none 처리한 뒤, 컨테이너에 아이템이 없을 때만 .list-container:not(:has(.list-item)) .empty-state { display: block; }로 드러내는 패턴을 씁니다. 목록을 비동기로 채우는 화면에서 로딩·빈 상태·정상 상태를 JS 분기 없이 표현할 수 있어 코드가 눈에 띄게 단순해집니다.

6.2 다크모드 토글 연동

html 태그에 :has()를 적용하면, 자식 요소의 상태에 따라 전역 테마를 전환할 수 있습니다. 체크박스 하나로 전체 페이지의 다크모드를 제어하는 것이 CSS만으로 가능해진 것입니다. 예를 들어 html:has(#theme-toggle:checked)를 앵커로 삼아 CSS 커스텀 프로퍼티(변수) 값을 바꾸면, 페이지 전체 색 토큰이 한 번에 전환됩니다. 다만 이 패턴은 앞서 말한 "전역 앵커" 주의와 정면으로 맞물립니다. 테마 토글은 사용자가 자주 누르는 요소가 아니므로 무효화 빈도가 낮아 실용적이지만, 같은 원리를 스크롤이나 호버처럼 초당 수십 번 바뀌는 상태에 적용하면 이야기가 달라집니다. 또한 사용자 시스템 설정을 존중하려면 prefers-color-scheme 미디어 쿼리를 기본값으로 두고, :has() 토글은 명시적 오버라이드로만 얹는 계층 설계가 안전합니다.

6.3 반응형 컴포넌트 레이아웃

컨테이너 쿼리(@container)와 :has()를 결합하면, 컴포넌트가 자신이 포함하는 콘텐츠에 따라 레이아웃을 자동으로 조절하는 "자기 인식 컴포넌트(Self-Aware Components)"를 만들 수 있습니다. 이미지가 있으면 2단 그리드, 없으면 1단 레이아웃 같은 패턴을 프레임워크 없이 구현할 수 있습니다. 두 기능의 관심사가 서로 다르다는 점을 기억하면 조합이 명확해집니다. 컨테이너 쿼리가 "내가 놓인 공간이 얼마나 넓은가"에 반응한다면, :has()는 "내가 담고 있는 콘텐츠가 무엇인가"에 반응합니다. 공간과 내용, 이 두 축을 함께 읽는 컴포넌트는 부모가 어떤 클래스를 내려주는지에 의존하지 않고 스스로 적절한 형태를 찾아갑니다. 이는 재사용 가능한 UI 라이브러리를 만들 때 특히 강력한 무기가 됩니다.

7. 성능 고려사항과 주의점

:has()는 강력하지만, 남용하면 성능 문제를 일으킬 수 있습니다. 브라우저는 :has() 내부의 선택자가 변경될 때마다 부모 요소의 스타일을 재계산해야 하므로, 깊은 DOM 트리에서 복잡한 :has() 선택자는 레이아웃 스래싱의 원인이 될 수 있습니다.

  • :has() 내부에 다른 :has()를 중첩하지 마세요. (이는 권장 사항이 아니라 명세상 금지입니다. 아래에서 다룹니다.)
  • 가능한 선택자의 범위를 좁히세요 (body:has(...)보다 .container:has(...)가 낫습니다).
  • 대규모 컴포넌트 트리에서는 contain: layout style 같은 CSS Containment 속성으로 재계산의 영향 범위를 컴포넌트 경계 안으로 격리하는 것도 도움이 됩니다.
  • 애니메이션이 관련된 경우, will-change 속성으로 브라우저에 힌트를 제공하세요.
  • 성능을 의심할 때는 브라우저 개발자 도구의 Performance 패널에서 "Recalculate Style" 항목의 소요 시간과 발생 빈도를 실제로 측정하세요. 추측이 아니라 계측이 최적화의 출발점입니다.

8. :has()로 할 수 없는 것 — 중첩·의사 요소·비관용 파싱

강력한 기능일수록 경계선을 아는 것이 중요합니다. MDN과 W3C 명세 기준으로 :has()에는 명확한 제약이 세 가지 있습니다.

첫째, :has()를 다른 :has() 안에 중첩할 수 없습니다. div:has(.a:has(.b)) 같은 선언은 명세상 허용되지 않습니다. 무한에 가까운 재귀적 관계 질의가 만들어내는 성능·구현 복잡도를 원천 차단하기 위한 규칙입니다. 표현하고 싶은 관계가 이런 형태라면 마크업을 조정하거나 조건을 나누는 편이 맞습니다.

둘째, 의사 요소(pseudo-element)는 :has()의 인수로도, :has()가 붙는 앵커로도 쓸 수 없습니다.div:has(::before)도, ::before:has(...)도 유효하지 않습니다. MDN이 설명하는 근거는, 상당수의 의사 요소가 조상의 스타일에 따라 조건적으로 존재하기 때문에 이를 :has()로 질의하면 순환 참조(cyclic querying)가 생길 수 있다는 것입니다.

셋째, :has()의 선택자 리스트는 비관용(non-forgiving) 파싱을 씁니다. 이 부분은 :is(), :where()와 다른 매우 중요한 함정입니다. :is(.valid, :nonsense-pseudo)처럼 관용(forgiving) 리스트는 목록 안에 유효하지 않은 선택자가 섞여 있어도 나머지로 매칭을 이어가지만, :has()는 그렇지 않습니다. 괄호 안 선택자 중 하나라도 브라우저가 이해하지 못하면 :has() 규칙 전체가 무효화됩니다. 벤더 프리픽스가 붙은 실험적 선택자나 오타 하나가 규칙 전체를 소리 없이 날려버릴 수 있으므로, :has() 안에는 확실히 지원되는 선택자만 넣는 습관이 필요합니다.

9. 브라우저 지원과 폴백 (2026년 기준)

2026년 현재 :has()Baseline "널리 사용 가능(Widely available)" 등급입니다. 즉 모든 주요 브라우저의 안정 버전에서 기본으로 동작하며, 실무 투입에 사실상 제약이 없습니다. 최초 도입 시점을 정리하면 Safari 15.4(2022년 3월)가 가장 앞섰고, Chrome과 Edge가 105(2022년), Firefox가 121(2023년 12월)에서 플래그 없이 지원을 시작했습니다. 모든 코어 브라우저가 지원을 갖춘 2023년 12월이 Baseline "새로 사용 가능(Newly available)"의 기준점이 되었고, 이후 충분한 안정화 기간을 거쳐 지금은 "널리 사용 가능" 단계에 올라섰습니다.

그럼에도 아주 오래된 브라우저나 특정 임베디드 웹뷰까지 감안해야 하는 프로젝트라면 폴백을 설계해두는 것이 안전합니다. 표준적인 방법은 기능 질의(feature query)입니다.

/* :has()를 지원하는 브라우저에서만 이 블록이 적용된다 */
@supports selector(:has(*)) {
  .card:has(.card-image) {
    grid-template-columns: 1fr 1fr;
  }
}

@supports selector(...)는 선택자 지원 여부를 조건으로 삼아, 미지원 브라우저에는 규칙을 아예 노출하지 않습니다. 반대로 미지원 브라우저에 기본 스타일을 먼저 깔고 지원 브라우저에서 향상시키는 점진적 향상(progressive enhancement) 방식으로 접근하면, 어느 쪽에서도 레이아웃이 깨지지 않습니다.

또 하나 알아둘 폴백 기법이 있습니다. 앞서 :has()의 리스트는 비관용이라고 했지만, :has() 자체를 관용 리스트인 :is():where()로 감싸면, :has()를 이해하지 못하는 브라우저에서 규칙 전체가 무효화되는 것을 막을 수 있습니다. 다만 이 경우 특이성이 :where()로 0이 되거나 :is() 규칙에 종속되므로, 특이성 영향을 반드시 함께 계산해야 합니다.

10. 자주 묻는 질문

Q. :has()를 쓰면 JavaScript가 정말로 필요 없어지나요?
아닙니다. :has()는 이미 DOM에 표현된 상태를 스타일로 읽어내는 데 탁월하지만, 데이터를 가져오거나, 서버와 통신하거나, 복잡한 애플리케이션 상태를 관리하는 일은 여전히 JS의 몫입니다. 우리의 목표는 JS를 제거하는 것이 아니라, "순수하게 스타일 표현에 불과한 로직"을 JS에서 걷어내 각 계층을 제자리로 돌려보내는 것입니다.

Q. :has()document.querySelector에서도 동작하나요?
네. :has()는 CSS 셀렉터일 뿐 아니라 표준 셀렉터 API에서도 사용할 수 있어 document.querySelector('.card:has(img)') 같은 질의가 가능합니다. 다만 이 문맥에서도 비관용 파싱이 적용되므로, 유효하지 않은 인수를 넣으면 예외가 발생합니다.

Q. :has()가 접근성에 영향을 주나요?
:has()는 시각적 스타일만 바꿀 뿐 접근성 트리(accessibility tree)나 시맨틱을 바꾸지 않습니다. 색이나 테두리로만 상태를 전달하면 보조 기술 사용자에게는 그 정보가 닿지 않습니다. 유효성 오류 같은 중요한 상태는 aria-invalid, role, 텍스트 안내를 함께 제공해야 하며, :has()는 그 위에 얹는 시각적 강화 정도로 이해하는 것이 옳습니다.

조금 더 나아가면 — :has()가 여는 가능성

:has()는 "부모를 선택할 수 없다"는 CSS의 오래된 제약을 깨뜨리고, JavaScript 의존도를 대폭 줄이는 선언적 UI 패턴의 폭을 크게 넓혔습니다. 컨테이너 쿼리, CSS Nesting과 함께 :has()를 마스터한다면, JavaScript에 의존하지 않고도 꽤 정교한 반응형 인터랙션을 구현할 수 있습니다. CSS의 르네상스는 지금 이 순간에도 진행 중입니다.


이 주제와 연결되는 글