:has(): 계열 선택기

CSS 용어로 말하자면, 오래전부터 다양한 의미에서 계층 구조를 사용해 왔습니다. Google의 스타일은 'CSS'(캐스캐이딩 스타일 시트)를 구성합니다. 선택기도 계단식입니다. 수평으로 이동할 수 있습니다. 대부분의 경우 하향 이동합니다. 하지만 위로 향하는 경우는 없습니다. YouTube는 오랫동안 '부모 선택 도구'를 꿈꿔 왔습니다. 이제 드디어 출시됩니다. :has() 가상 선택기 형태입니다.

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

하지만 '상위' 선택기 이상의 의미가 있습니다. 좋은 마케팅 방법입니다. '조건부 환경' 선택기가 그다지 매력적이지 않을 수 있습니다. 하지만 그건 뭔가 느낌이 다릅니다. '가족' 선택기는 어떨까요?

브라우저 지원

계속하기 전에 브라우저 지원에 관해 언급할 만합니다. 아직은 그렇지 않습니다. 하지만 점점 가까워지고 있습니다. 아직 Firefox는 지원되지 않지만 향후 지원될 예정입니다. 하지만 이미 Safari에 있으며 Chromium 105에서 출시될 예정입니다. 이 도움말의 모든 데모는 사용 중인 브라우저에서 지원되지 않는지 알려줍니다.

를 참고하세요.

:has 사용 방법

그렇다면 혁신 문화란 무엇일까요? 다음 HTML을 살펴보세요. 이 HTML에는 everybody 클래스가 있는 두 개의 상위 요소가 있습니다. 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를 선택합니다. css a:not(:has(> svg)) { ... } 직접 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()의 큰 이점입니다. :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()할 수 없습니다. 하지만 :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)) { … }
  • 가상 요소 css ::part(foo):has(:focus) { … } 뒤의 :has() 사용 제한
  • :visited 사용은 항상 false입니다. css :has(:visited) { … }

:has()와 관련된 실제 실적 측정항목은 이 글리치를 확인하세요. 구현에 관한 유용한 정보와 세부정보를 공유해 주신 병우님께 감사드립니다.

완료되었습니다.

:has()님을 기다리는 중... 친구에게 이 소식을 전하고 이 게시물을 공유해 주세요. CSS에 접근하는 방식에 큰 변화가 있을 것입니다.

모든 데모는 이 CodePen 컬렉션에서 확인할 수 있습니다.