:has(): 계열 선택기

CSS 측면에서 봤을 때 세월이 시작된 이래로 다양한 의미로 폭포식 구조를 작업해 왔습니다. Google의 스타일은 '하단 스타일 시트'를 구성합니다. 선택기도 하위로 전파됩니다. 옆으로 이동할 수 있습니다. 대부분의 경우 아래로 내려갑니다. 그러나 결코 위로 향하지는 않습니다. Google은 여러 해 동안 '부모 선택기'를 사용해 왔습니다. 그리고 드디어 등장합니다! :has() 의사 선택기 모양입니다.

매개변수로 전달된 선택기 중 하나가 하나 이상의 요소와 일치하는 경우 :has() CSS 의사 클래스가 요소를 나타냅니다.

단순한 '부모' 이상의 선택기가 표시됩니다. 이를 마케팅하기에 좋은 방법이라고 할 수 있습니다. 그다지 매력적이지 않은 방법은 '조건부 환경'입니다. 선택기가 표시됩니다. 하지만 그 둘은 똑같은 고리를 갖고 있지 않습니다. '가족'은 어떤가요? 선택기?

브라우저 지원

계속 진행하기 전에 브라우저 지원에 대해 언급하는 것이 좋습니다. 아직 도달하지 못했습니다. 하지만 점점 가까워지고 있습니다. Firefox는 아직 지원되지 않으며 향후 지원할 예정입니다. 그러나 이미 Safari에 있으며 Chromium 105에 출시될 예정입니다. 이 도움말의 모든 데모는 사용 중인 브라우저에서 지원되지 않는지 여부를 알려줍니다.

사용 방법 :has

그렇다면 혁신 문화란 무엇일까요? everybody 클래스가 있는 두 동위 요소가 있는 다음 HTML을 생각해 보세요. a-good-time 클래스의 하위 요소가 있는 항목을 어떻게 선택하나요?

<div class="everybody">
  <div>
    <div class="a-good-time"></div>
  </div>
</div>

<div class="everybody"></div>

:has()를 사용하면 다음 CSS로 이 작업을 수행할 수 있습니다.

.everybody:has(.a-good-time) {
  animation: party 21600s forwards;
}

그러면 .everybody의 첫 번째 인스턴스가 선택되고 animation가 적용됩니다.

이 예에서는 everybody 클래스를 가진 요소가 타겟입니다. 조건에 a-good-time 클래스의 하위 요소가 있습니다.

<target>:has(<condition>) { <styles> }

하지만 :has()은(는) 많은 기회를 열어주기 때문에 그보다 훨씬 더 넓은 범위를 활용할 수 있습니다. 심지어 아직 발견되지 않았을 가능성이 높습니다. 다음 중 몇 가지를 고려해 보세요.

직접 figcaption가 있는 figure 요소를 선택합니다. css figure:has(> figcaption) { ... } 직접 SVG 하위 요소가 없는 anchor 선택 <ph type="x-smartling-placeholder">css a:not(:has(> svg)) { ... }</ph> 직접 input 동위 요소가 있는 label를 선택합니다. 옆으로 가고 있어! css label:has(+ input) { … } 하위 요소 imgalt 텍스트가 없는 article 선택 css article:has(img:not([alt])) { … } DOM에 일부 상태가 있는 documentElement 선택 css :root:has(.menu-toggle[aria-pressed=”true”]) { … } 하위 요소가 홀수인 레이아웃 컨테이너를 선택합니다. css .container:has(> .container__item:last-of-type:nth-of-type(odd)) { ... } 그리드에서 마우스 오버되지 않은 모든 항목을 선택합니다. css .grid:has(.grid__item:hover) .grid__item:not(:hover) { ... } 맞춤 요소가 포함된 컨테이너를 선택합니다 <todo-list> css main:has(todo-list) { ... } 직계 동위 hr 요소가 있는 단락 내에서 모든 솔로 a를 선택합니다. css p:has(+ hr) a:only-child { … } 여러 조건이 충족되는 article 선택 css article:has(>h1):has(>h2) { … } 섞어보세요. 제목 뒤에 부제목이 오는 article를 선택합니다. css article:has(> h1 + h2) { … } 대화형 상태가 트리거될 때 :root 선택 css :root:has(a:hover) { … } figcaption이 없는 figure 다음에 오는 단락을 선택합니다. css figure:not(:has(figcaption)) + p { … }

:has()의 흥미로운 사용 사례에는 어떤 것이 있을까요? 여기서 흥미로운 점은 정신적 모델을 깨뜨리라고 격려한다는 것입니다. '이 스타일에 다른 방식으로 접근할 수 있을까?'라는 생각이 들게 됩니다.

몇 가지 사용 사례를 살펴보겠습니다.

카드

기본 카드 데모를 보겠습니다. 카드에 제목, 부제목, 일부 미디어와 같은 모든 정보를 표시할 수 있습니다. 여기 기본 카드가 있습니다.

<li class="card">
  <h2 class="card__title">
      <a href="#">Some Awesome Article</a>
  </h2>
  <p class="card__blurb">Here's a description for this awesome article.</p>
  <small class="card__author">Chrome DevRel</small>
</li>

미디어를 소개하면 어떻게 되나요? 이 디자인에서는 카드를 두 개의 열로 분할할 수 있습니다. 이전에는 이 동작을 나타내는 새 클래스(예: card--with-media 또는 card--two-columns)를 만들 수 있습니다. 이러한 클래스 이름은 기억하기 어려울 뿐만 아니라 유지관리하고 기억하기도 어려워집니다.

:has()를 사용하면 카드에 미디어가 있음을 감지하고 적절한 작업을 실행할 수 있습니다. 수정자 클래스 이름은 필요하지 않습니다.

<li class="card">
  <h2 class="card__title">
    <a href="/article.html">Some Awesome Article</a>
  </h2>
  <p class="card__blurb">Here's a description for this awesome article.</p>
  <small class="card__author">Chrome DevRel</small>
  <img
    class="card__media"
    alt=""
    width="400"
    height="400"
    src="./team-awesome.png"
  />
</li>

그리고 거기에 그대로 둘 필요가 없습니다. 창의력을 발휘할 수 있습니다. '추천' 콘텐츠가 표시되는 카드는 레이아웃 내에서 어떻게 조정되나요? 이 CSS는 추천 카드를 레이아웃의 전체 너비로 만들고 그리드의 시작 부분에 배치합니다.

.card:has(.card__banner) {
  grid-row: 1;
  grid-column: 1 / -1;
  max-inline-size: 100%;
  grid-template-columns: 1fr 1fr;
  border-left-width: var(--size-4);
}

배너가 있는 추천 카드가 사용자의 관심을 끌기 위해 흔들리면 어떻게 되나요?

<li class="card">
  <h2 class="card__title">
    <a href="#">Some Awesome Article</a>
  </h2>
  <p class="card__blurb">Here's a description for this awesome article.</p>
  <small class="card__author">Chrome DevRel</small>
  <img
    class="card__media"
    alt=""
    width="400"
    height="400"
    src="./team-awesome.png"
  />
  <div class="card__banner"></div>
</li>
.card:has(.card__banner) {
  --color: var(--green-3-hsl);
  animation: wiggle 6s infinite;
}

무궁무진한 가능성

양식

양식은 어떨까요? 스타일 지정이 어려운 것으로 알려져 있습니다. 한 가지 예는 입력과 라벨의 스타일을 지정하는 것입니다. 예를 들어 필드가 유효한지 어떻게 알릴 수 있나요? :has()를 사용하면 작업이 훨씬 쉬워집니다. 관련 양식 의사 클래스(예: :valid:invalid)에 연결할 수 있습니다.

<div class="form-group">
  <label for="email" class="form-label">Email</label>
  <input
    required
    type="email"
    id="email"
    class="form-input"
    title="Enter valid email address"
    placeholder="Enter valid email address"
  />   
</div>
label {
  color: var(--color);
}
input {
  border: 4px solid var(--color);
}

.form-group:has(:invalid) {
  --color: var(--invalid);
}

.form-group:has(:focus) {
  --color: var(--focus);
}

.form-group:has(:valid) {
  --color: var(--valid);
}

.form-group:has(:placeholder-shown) {
  --color: var(--blur);
}

이 예제에서 시도해 보세요. 유효한 값과 잘못된 값을 입력하고 포커스를 맞추고 끄세요.

:has()를 사용하여 필드의 오류 메시지를 표시하거나 숨길 수도 있습니다. '이메일' 필드 그룹에 오류 메시지를 추가합니다.

<div class="form-group">
  <label for="email" class="form-label">
    Email
  </label>
  <div class="form-group__input">
    <input
      required
      type="email"
      id="email"
      class="form-input"
      title="Enter valid email address"
      placeholder="Enter valid email address"
    />   
    <div class="form-group__error">Enter a valid email address</div>
  </div>
</div>

기본적으로 오류 메시지는 숨겨집니다.

.form-group__error {
  display: none;
}

그러나 필드가 :invalid가 되고 포커스가 없으면 추가 클래스 이름을 사용하지 않고도 메시지를 표시할 수 있습니다.

.form-group:has(:invalid:not(:focus)) .form-group__error {
  display: block;
}

사용자가 양식과 상호작용할 때 멋진 기발함을 더할 수 없을 이유가 없습니다. 다음 예에 관해 생각해 보세요. 마이크로 상호작용에 유효한 값을 입력할 때 시청합니다. :invalid 값이 있으면 양식 그룹이 흔들립니다. 그러나 사용자에게 모션 환경설정이 없는 경우에만 가능합니다.

콘텐츠

코드 예시에서 이에 관해 다루었습니다. 하지만 문서 흐름에서 :has()를 어떻게 사용할 수 있을까요? 예를 들어 미디어의 서체 스타일에 관한 아이디어를 얻을 수 있습니다.

figure:not(:has(figcaption)) {
  float: left;
  margin: var(--size-fluid-2) var(--size-fluid-2) var(--size-fluid-2) 0;
}

figure:has(figcaption) {
  width: 100%;
  margin: var(--size-fluid-4) 0;
}

figure:has(figcaption) img {
  width: 100%;
}

이 예에는 그림이 포함되어 있습니다. figcaption가 없으면 콘텐츠 내에서 플로팅됩니다. figcaption가 있으면 전체 너비를 차지하여 추가 여백을 얻습니다.

상태에 반응

마크업의 일부 상태에 대해 스타일이 반응하도록 만드는 것은 어떨까요? '클래식' 클래스와 탐색 메뉴가 있습니다. 탐색 열기를 전환하는 버튼이 있는 경우 aria-expanded 속성을 사용할 수 있습니다. JavaScript를 사용하여 적절한 속성을 업데이트할 수 있습니다. aria-expandedtrue이면 :has()를 사용하여 이를 감지하고 슬라이딩 탐색 스타일을 업데이트합니다. JavaScript는 자신의 역할을 수행하며 CSS는 해당 정보로 원하는 작업을 할 수 있습니다. 마크업을 셔플하거나 클래스 이름을 추가하는 등의 작업을 할 필요가 없습니다(참고: 프로덕션용 예시는 아님).

:root:has([aria-expanded="true"]) {
    --open: 1;
}
body {
    transform: translateX(calc(var(--open, 0) * -200px));
}

사용자 오류를 방지하는 데 도움이 될 수 있나요?

이러한 예시의 공통점은 무엇일까요? :has() 사용 방법을 보여주는 것 외에는 클래스 이름을 수정할 필요가 없습니다. 각각 새 콘텐츠를 삽입하고 속성을 업데이트했습니다. 이는 사용자 오류를 완화하는 데 도움이 된다는 점에서 :has()의 큰 장점입니다. :has()를 사용하면 CSS가 DOM의 수정사항을 조정하는 책임을 맡을 수 있습니다. JavaScript에서 클래스 이름을 번갈아 사용할 필요가 없으므로 개발자 오류가 발생할 가능성이 줄어듭니다. 클래스 이름을 오타로 남겼는데 이를 Object 조회로 유지해야 했던 경험이 있을 것입니다.

이는 흥미로운 생각이고, 마크업이 더 깔끔해지고 코드를 더 적게 사용할 수 있나요? JavaScript 조정을 많이 하지 않으므로 JavaScript가 줄어듭니다. card card--has-media 등의 클래스가 더 이상 필요하지 않으므로 HTML이 적습니다.

고정관념을 깨기

위에서 언급했듯이 :has()는 멘탈 모델을 깨도록 권장합니다. 다양한 시도를 할 수 있는 기회입니다. 이처럼 경계를 넓히기 위한 한 가지 방법은 CSS만으로 게임 메커니즘을 만드는 것입니다. 예를 들어 양식과 CSS를 사용하여 단계 기반 메커니즘을 만들 수 있습니다.

<div class="step">
  <label for="step--1">1</label>
  <input id="step--1" type="checkbox" />
</div>
<div class="step">
  <label for="step--2">2</label>
  <input id="step--2" type="checkbox" />
</div>
.step:has(:checked), .step:first-of-type:has(:checked) {
  --hue: 10;
  opacity: 0.2;
}


.step:has(:checked) + .step:not(.step:has(:checked)) {
  --hue: 210;
  opacity: 1;
}

그리고 이를 통해 흥미로운 가능성이 열립니다. 이를 사용하여 변환을 통해 양식을 순회할 수 있습니다. 이 데모는 별도의 브라우저 탭에서 보는 것이 가장 좋습니다.

재미있는 클래식 버즈 와이어 게임은 어떠신가요? :has()를 사용하면 메커니즘을 더 쉽게 만들 수 있습니다. 유선에 마우스를 가져가면 게임이 종료됩니다. 예. 동위 조합자 (+~)와 같은 요소로 게임 메커니즘을 만들 수 있습니다. 그러나 :has()를 사용하면 흥미로운 마크업 '트릭'을 사용하지 않고도 동일한 결과를 얻을 수 있습니다. 이 데모는 별도의 브라우저 탭에서 보는 것이 가장 좋습니다.

조만간 프로덕션에 배포하진 않겠지만 프리미티브를 사용할 수 있는 방법을 강조합니다. 예를 들어 :has()를 체이닝할 수 있습니다.

:root:has(#start:checked):has(.game__success:hover, .screen--win:hover)
.screen--win {
  --display-win: 1;
}
드림

성능 및 제한사항

시작하기 전에 :has()(으)로 할 수 없는 작업은 무엇인가요? :has()에는 몇 가지 제한사항이 있습니다. 주요 원인은 성능 히트로 인해 발생합니다.

  • :has():has()할 수는 없습니다. 그러나 :has()를 체이닝할 수 있습니다. css :has(.a:has(.b)) { … }
  • :has() 내 유사 요소 사용 없음 css :has(::after) { … } :has(::first-letter) { … }
  • 복합 선택기만 허용하는 의사 내에서 :has() 사용 제한 css ::slotted(:has(.a)) { … } :host(:has(.a)) { … } :host-context(:has(.a)) { … } ::cue(:has(.a)) { … }
  • 의사 요소 뒤의 :has() 사용 제한 css ::part(foo):has(:focus) { … }
  • :visited를 사용하면 항상 false입니다. css :has(:visited) { … }

:has()와 관련된 실제 성능 측정항목은 이 Glitch를 확인하세요. 구현에 대한 정보와 세부정보를 공유해 준 병우 씨의 이름이기도 합니다.

완료되었습니다.

:has()님을 위해 준비하세요. 친구들에게 이 기능을 알리고 이 게시물을 공유하세요. 이 게시물은 CSS에 대한 접근 방식의 판도를 바꿀 것입니다.

모든 데모는 이 CodePen 자료에서 확인할 수 있습니다.