: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 선택 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) { ... } {1/4}articleahrcss p:has(+ hr) a:only-child { … }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)) { … }
  • 유사 요소 css ::part(foo):has(:focus) { … } 뒤의 :has() 사용 제한
  • :visited를 사용하면 항상 false입니다. css :has(:visited) { … }

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

간단하죠?

:has()을(를) 준비하세요. 친구에게 이 게시물을 공유하고 이 게시물을 공유하세요. 이 게시물은 우리가 CSS에 접근하는 방식에 획기적인 변화를 일으킬 것입니다.

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