C.W.K.
Stream
Lesson 05 of 05 · published

:is(), :where(), :has() — 규칙을 다시 쓴 selector 들

~12 min · is, where, has, modern-css, logical-selectors

Level 0Markup Novice
0 XP0/34 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"20 년 동안 CSS 가 같은 selector 를 세 번 쓰라고 시켰어. 그러더니 함수 네 개가 와서 그 의식의 절반을 조용히 은퇴시켰어."

:is() 가 해결한 문제

:is() 없으면 이렇게 적었어:

article h1, article h2, article h3,
section h1, section h2, section h3,
aside h1, aside h2, aside h3 {
  font-family: 'Display', serif;
}

Selector 9 개, 다 "콘텐츠 섹션 안의 어떤 heading". 실수 하나, 9 번 반복. :is() 로:

:is(article, section, aside) :is(h1, h2, h3) {
  font-family: 'Display', serif;
}

같은 결과. 9 개 조합 대신 두 그룹. 교차곱이 단일 가독 표현으로 무너져.

:is() Specificity 세부

:is() 가 가장 specific 한 argument 의 specificity 받음. :is(.btn, #cta)(0,1,0,0)#cta 가 가장 높아서. Specificity 가 평균될 거라 기대한 작성자를 가끔 놀라게 해. 규칙은 간단: 최대, 평균이 아님.

그 escalation 안 원할 때 — 모든 argument 가 zero specificity 기여하길 원할 때 — :where() 써.

:where() — Specificity 지우개

:where(selectors):is(selectors) 와 같은 element 매치하지만 안에 뭐 있든 specificity (0,0,0,0) 기여. 디자인 시스템이 필요로 하는 정확히 그것: 사소하게 override 가능한 베이스 스타일.

Selector 리스트 가진 :not()

모던 :not() 이 selector 리스트 받음:

li:not(:first-child, :last-child) { /* 모든 가운데 항목 */ }
button:not([disabled], [aria-disabled="true"]):hover { /* hover 상태 */ }

옛 형식 (:not() 마다 selector 하나) 도 여전히 작동하지만 리스트 형식이 짧고 읽기 쉬워. Specificity 규칙: :is() 와 같음 — 가장 높은 argument 의 specificity 받음.

:has() — 웹이 빌어 온 부모 selector

20 년 동안 가장 많이 요청된 CSS 기능이 "부모 selector" — element 를 안의 내용 기반으로 스타일링하는 방법. 답은 항상 JavaScript: 자식 바뀔 때 부모에 class 토글. :has() 가 2023 년에 마침내 native 로 도착 (모든 모던 브라우저에 baseline 지원):

  • .card:has(img) — 이미지 포함하는 어떤 .card.
  • article:has(> h1) — 직접 <h1> 자식 가진 article.
  • form:has(input:invalid) — 적어도 하나의 유효하지 않은 input 가진 폼.
  • label:has(input[required])::after — required input 감싸는 label 에 빨간 별표 추가.
  • li:has(+ li:hover) — hover 중인 거 바로 앞 항목.
  • html:has(body.dark) — body 의 class 기반으로 html element 스타일링.

:has() 안의 관계는 어떤 combinator 든 (descendant 공백, child >, sibling +, ~). 진정한 관계 selector.

:has() 가 풀어낸 패턴

JavaScript 필요했던 일부 인터랙션이 이제 순수 CSS:

  • 유효하지 않은 필드 가진 폼 → disabled 처럼 보이는 submit 버튼. form:has(:invalid) button[type="submit"] { opacity: 0.5; }
  • 메인 콘텐츠 비어 있으면 사이드바 접힘. .layout:has(main:empty) aside { display: none; }
  • 자식 링크 hover 되면 카드 하이라이트. .card:has(a:hover) { background: var(--accent); }
  • Body class 기반 테마 반전. html:has(body.dark) { color-scheme: dark; }
  • 첨부 가진 대화에 'has notes' 인디케이터 표시. .conversation:has(.attachment) .indicator { display: inline; }
2023 년부터 :has() 가 widely supported. 모든 모던 브라우저 (Chrome, Edge, Safari, Firefox) 가 출시. 모던 브라우저만 타깃 중이라면 — 2026 년에 그래야 하는데 — :has() 가 일급 도구. CSS-as-DOM-projection 시대 끝났어.

:nth-child(of selector) — 또 다른 모던 파워

:nth-child(2n) 이 type 무시하고 모든 sibling 중에 셈. :nth-of-type(2n) 이 같은 element type sibling 만 셈. 새 :nth-child(2n of .card) 가 selector 매치하는 sibling 만 셈 — ".card 매치하는 매 다른 element". 이전엔 스크립트 없이 불가능:

2026 년 Selector 퍼포먼스

복잡한 selector 의 퍼포먼스 비용이 거의 모든 사이트에 사라질 만큼 작아. 모던 브라우저 엔진 (Blink, Gecko, WebKit) 이 공격적 최적화 가짐: selector 캐싱, ancestor invalidation, has-state tracking. "퍼포먼스 위해 descendant selector 피해" 옛 조언은 outdated. 가장 명료한 selector 적어. 문제 측정한 경우에만 프로파일.

브라우저 지원, 깔끔하게

Baseline 2023 기능 (주요 브라우저에 widely available): :is(), :where(), :has(), selector 리스트 가진 :not(), :nth-child(of selector). 써. Caniuse.com 이 특정 옛 브라우저 요구 있으면 per-browser 버전 cutoff 알려 줘.

피파의 노트

피파의 WebUI 가 :has() 위에 전적으로 지어진 패턴 둘: (1) 채팅 메시지 리스트가 li:has(.attachment) .badge 써서 'has attachment' 배지 추가 — JavaScript class 토글 없음, observer 없음. (2) council UI 가 brain 선택 안 됐을 때 picker 흐릿하게: .council-panel:has(.brain-select:invalid) .picker { opacity: 0.5; }. 옛 접근법 (상태 구독, class 토글) 이 이제 그냥 CSS selector. 코드 적게, 버그 적게, 렌더 빠르게.

Code

:is(), :where(), :not() — 교차곱 가족·css
/* :is() — selector 교차곱 무너뜨리기 */

/* 전: selector 9 개 */
article h1, article h2, article h3,
section h1, section h2, section h3,
aside h1, aside h2, aside h3 {
  font-family: 'Display', serif;
}

/* 후: 표현 하나 */
:is(article, section, aside) :is(h1, h2, h3) {
  font-family: 'Display', serif;
}

/* :where() — 같은 매칭, zero specificity, 베이스 스타일에 완벽 */
:where(article, section, aside) :where(h1, h2, h3) {
  font-family: 'Display', serif;
  margin: 0 0 0.5em;
}

/* 리스트 가진 :not() */
li:not(:first-child, :last-child, .featured) {
  border-bottom: 1px solid #eee;
}
:has() — JS 필요했던 패턴들·css
/* :has() — 부모 selector */

/* 이미지 가진 카드는 다른 배경 */
.card:has(img) {
  background: linear-gradient(180deg, #f8f8f8, transparent);
}

/* 유효하지 않은 필드 가진 폼은 disabled 처럼 보이는 submit */
form:has(:invalid) button[type="submit"] {
  opacity: 0.5;
  cursor: not-allowed;
}

/* Required input 감싸는 label 에 빨간 별표 */
label:has(> input[required])::after {
  content: ' *';
  color: #d93b3b;
}

/* 페이지 레이아웃: main 이 비면 aside 숨김 */
.layout:has(main:empty) aside {
  display: none;
}

/* 자식 링크 hover 되면 카드 켜짐 */
.card:has(a:hover) {
  outline: 2px solid var(--accent);
  outline-offset: 4px;
}

/* Body class 기반 html dark mode (JS 안 필요) */
html:has(body.dark) { color-scheme: dark; }
html:has(body.light) { color-scheme: light; }
:nth-child(of selector) — 선택적 세기·css
/* :nth-child(of selector) — 매치하는 sibling 만 세기 */

/* 보이는 카드만 zebra stripe */
.card:nth-child(odd of :not([hidden])) {
  background: #f8f8f8;
}

/* .featured sibling 중 3 번째 항목 */
.item:nth-child(3 of .featured) {
  font-weight: 700;
}

/* 옛 행동과 비교 */
.card:nth-child(odd) { /* 모든 sibling 중 매 홀수, 숨겨진 거 포함 */ }

External links

Exercise

사이트의 실제 폼 가져오기 (또는 sign-up 폼 만들기). :has() 써서: (1) 어떤 필드든 :invalid 면 submit 버튼 비활성화, (2) 첫 유효하지 않은 input 포함하는 fieldset 에 빨간 테두리, (3) 유효하지 않은 필드 있을 때만 폼 위에 '입력 검토하세요' 메시지 표시. 다 순수 CSS, JavaScript 없이. 그러고 나서 다음 개발자가 단일 class 로 override 가능한 .field 의 :where() 베이스 스타일 추가.
Hint
disabled 상태는 form:has(:invalid) button[type="submit"] { ... }. 빨간 테두리는 fieldset:has(:invalid:first-of-type), 단 fieldset 들 걸친 '폼의 첫 유효하지 않은 거' 는 더 생각 필요. '입력 검토하세요' 메시지는 .form-warning { display: none; } 과 form:has(:invalid) ~ .form-warning { display: block; } 로 가능.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.