:has(): selektor rodziny

Od początku istnienia świata (w terminach CSS) używamy kaskady w różnych znaczeniach. Nasze style tworzą „Arkusz stylów kaskadowych”. Nasze selektory też są kaskadowe. Mogą się one znajdować na boku. W większości przypadków spadają. Ale nigdy nie w górę. Od lat fantazjowaliśmy o „selektorze rodzica”. I wreszcie nadszedł ten moment. w postaci pseudoselektora :has().

Pseudoklasa CSS :has() reprezentuje element, jeśli którykolwiek z selektorów przekazanych jako parametry pasuje do co najmniej jednego elementu.

To jednak coś więcej niż selektor „rodzic”. To dobry sposób na reklamę. Niezbyt atrakcyjnym rozwiązaniem może być selektor „środowisko warunkowe”. Ale to nie brzmi tak samo. A co z selektorem „rodzina”?

Obsługa przeglądarek

Zanim przejdziemy dalej, warto wspomnieć o obsłudze przeglądarki. Jeszcze nie. Ale zbliża się do nas. Firefox nie jest jeszcze obsługiwany, ale pracujemy nad tym. Jest ona już dostępna w Safari i ma zostać wydana w Chromium 105. W przypadku wszystkich demonstracji w tym artykule znajdziesz informacje, czy są one obsługiwane w używanej przeglądarce.

Jak używać :has

Jak to wygląda? Weź pod uwagę ten kod HTML z 2 elementami braćmi z klasą everybody. Jak wybrać element potomny klasy a-good-time?

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

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

W przypadku :has() możesz to zrobić za pomocą tego kodu CSS.

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

Wybiera pierwszą instancję .everybody i zastosowuje animation.

W tym przykładzie element z klasą everybody jest celem. Warunek: musi istnieć potomek klasy a-good-time.

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

Możesz jednak zrobić znacznie więcej, ponieważ :has() otwiera wiele możliwości. Nawet te, które prawdopodobnie nie zostały jeszcze odkryte. Rozważ te opcje.

Wybierz elementy figure, które mają bezpośrednie połączenie z elementem figcaption. css figure:has(> figcaption) { ... }Wybierz anchor, które nie mają bezpośredniego potomka SVG.css a:not(:has(> svg)) { ... }Wybierz label, które mają bezpośredni element input. Skręcamy w bok. css label:has(+ input) { … } Wybierz article, w których potomku img nie ma tekstu alt css article:has(img:not([alt])) { … } Wybierz documentElement, w którym występuje jakiś stan w DOM css :root:has(.menu-toggle[aria-pressed=”true”]) { … } Wybierz kontener układu z nieparzystą liczbą elementów potomnych css .container:has(> .container__item:last-of-type:nth-of-type(odd)) { ... } Wybierz wszystkie elementy w siatce, które nie są podświetlone css .grid:has(.grid__item:hover) .grid__item:not(:hover) { ... } Wybierz kontener, który zawiera element niestandardowy <todo-list> css main:has(todo-list) { ... } Wybierz wszystkie pojedyncze a w akapicie, które mają bezpośredni element nadrzędny hr css p:has(+ hr) a:only-child { … } Wybierz article, w których spełnione są różne warunki css article:has(>h1):has(>h2) { … } Zmien kolejność. Wybierz article, gdzie tytuł jest poprzedzony przez napis.css article:has(> h1 + h2) { … }Wybierz :root, gdy są wywoływane stany interaktywne.css :root:has(a:hover) { … }Wybierz akapit poprzedzający figure, który nie zawiera figcaption.css figure:not(:has(figcaption)) + p { … }

Jakie interesujące przypadki użycia :has() możesz podać? Najciekawsze jest to, że zachęca ona do złamania modelu mentalnego. Zastanawiasz się, czy nie można by było użyć innego stylu.

Przykłady

Przyjrzyjmy się kilku przykładom użycia tego narzędzia.

Karty

Obejrzyj demonstrację klasycznej karty. Możemy wyświetlać na karcie dowolne informacje, np. tytuł, podtytuł lub materiały multimedialne. Oto karta podstawowa.

<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>

Co się dzieje, gdy chcesz wprowadzić jakiś materiał? W tym przypadku karta może być podzielona na 2 kolumny. Wcześniej możesz utworzyć nową klasę, która będzie reprezentować to zachowanie, na przykład card--with-media lub card--two-columns. Nazwy klas nie tylko stają się trudne do wymyślenia, ale też trudne do utrzymania i zapamiętania.

Dzięki :has() możesz wykryć, że karta zawiera treści multimedialne, i odpowiednio się zachować. Nie trzeba używać nazw klas modyfikatorów.

<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>

Nie musisz ich usuwać. Możesz też podejść do tematu kreatywnie. Jak karta z „polecanymi” treściami może pasować do układu? Ten kod CSS sprawi, że karta z polecanymi produktami będzie miała pełną szerokość układu i będzie znajdować się na początku siatki.

.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);
}

Co zrobić, jeśli wyróżniona karta z banerem miga, aby zwrócić uwagę?

<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;
}

Tak wiele możliwości.

Formularze

A formularze? Są one trudne do stylizacji. Jednym z takich przykładów jest stylizacja danych wejściowych i ich etykiet. Jak sygnalizujemy, że pole jest prawidłowe? Dzięki :has() jest to znacznie łatwiejsze. Możemy podłączyć się do odpowiednich pseudoklas formularza, np. :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);
}

Wypróbuj to na przykładzie: wpisz prawidłowe i nieprawidłowe wartości oraz włącz i wyłącz fokus.

Możesz też użyć :has(), aby wyświetlić lub ukryć komunikat o błędzie w polu. Weź grupę pól „e-mail” i dodaj do niej komunikat o błędzie.

<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>

Domyślnie komunikat o błędzie jest ukryty.

.form-group__error {
  display: none;
}

Gdy jednak pole stanie się :invalid i nie będzie aktywne, możesz wyświetlić wiadomość bez konieczności podawania dodatkowych nazw klas.

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

Możesz też dodać odrobinę fantazji, gdy użytkownicy będą wchodzić w interakcję z formularzem. Rozważ ten przykład. Zwróć uwagę, kiedy wpisujesz prawidłową wartość mikrointerakcji. Wartość :invalid spowoduje drżenie grupy formularzy. Tylko wtedy, gdy użytkownik nie ma ustawień dotyczących ruchu.

Treść

Omówiliśmy to w przykładzie kodu. Jak możesz jednak używać :has() w procesie tworzenia dokumentu? Podaje ona pomysły na to, jak możemy na przykład zastosować typografię w przypadku multimediów.

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%;
}

Ten przykład zawiera rysunki. Jeśli nie mają figcaption, są widoczne w treści. Gdy jest obecna, zajmuje całą szerokość i otrzymuje dodatkową margines.figcaption

Reagowanie na stan

Czy można stworzyć style, które będą reagować na stan znaczników? Rozważ przykład z „klasycznym” przesuwanym paskiem nawigacji. Jeśli masz przycisk, który przełącza się między trybami otwierania nawigacji, może on używać atrybutu aria-expanded. Aby zaktualizować odpowiednie atrybuty, możesz użyć JavaScriptu. Gdy aria-expanded to true, użyj :has(), aby wykryć tę wartość i zaktualizować style nawigacji przesuwnej. Kod JavaScript wykonuje swoją część pracy, a kod CSS może robić z tymi informacjami, co chce. Nie musisz zmieniać kolejności znaczników ani dodawać dodatkowych nazw klas. (Uwaga: to nie jest gotowy przykład).

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

Czy :has może pomóc uniknąć błędów użytkownika?

Co łączy te przykłady? Oprócz tego, że pokazują sposoby używania :has(), żaden z nich nie wymagał modyfikacji nazw klas. Każdy z nich wstawił nowe treści i zaktualizował atrybut. To świetna zaleta :has(), ponieważ może pomóc w zmniejszeniu liczby błędów popełnianych przez użytkowników. Dzięki :has() CSS może przejąć odpowiedzialność za dostosowywanie się do zmian w DOM. Nie musisz zmieniać nazw klas w JavaScript, co zmniejsza ryzyko popełnienia błędu przez programistę. Wszyscy popełniamy błędy przy wpisywaniu nazw zajęć i musimy korzystać z wyszukiwania Object.

To ciekawa myśl. Czy prowadzi ona do czystszego znacznika i mniejszego kodu? Mniej kodu JavaScript, ponieważ nie wprowadzamy tak wielu zmian w tym kodzie. Mniej kodu HTML, ponieważ nie są już potrzebne klasy takie jak card card--has-media itp.

Myślenie nieszablonowe

Jak wspomnieliśmy powyżej, :has() zachęca do złamania modelu mentalnego. To okazja do wypróbowania różnych rzeczy. Jednym ze sposobów na przesuwanie granic jest tworzenie mechaniki gry tylko za pomocą CSS. Możesz na przykład utworzyć mechanizm krokowy za pomocą formularzy i 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;
}

To otwiera ciekawe możliwości. Możesz go użyć do przejścia przez formularz za pomocą transformacji. Pamiętaj, że tę prezentację najlepiej wyświetlać w osobnej karcie przeglądarki.

A na koniec coś dla zabawy – klasyczna gra z drutem elektrycznym. Mechanika jest łatwiejsza do stworzenia za pomocą :has(). Jeśli kursor znajdzie się na drucie, gra się kończy. Tak, niektóre z tych mechanik gry można tworzyć za pomocą elementów takich jak kombinatory (+~). Jednak :has() to sposób na uzyskanie tych samych wyników bez konieczności stosowania ciekawych „sztuczek” związanych z tagowaniem. Pamiętaj, że tę prezentację najlepiej wyświetlać w osobnej karcie przeglądarki.

Nie będziesz ich prawdopodobnie wkrótce wdrażać w wersji produkcyjnej, ale pokazują one sposoby użycia prymitywu. Na przykład możliwość łańcuchowania :has().

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

Wydajność i ograniczenia

Zanim się rozłączymy, powiedz, czego nie można zrobić za pomocą :has()? W przypadku usługi :has() obowiązują pewne ograniczenia. Główne problemy wynikają z ograniczeń wydajności.

  • Nie możesz :has() :has(). Możesz jednak użyć :has(). css :has(.a:has(.b)) { … }
  • Brak pseudoelementów w elemencie :has() css :has(::after) { … } :has(::first-letter) { … }
  • Ogranicz użycie :has() w pseudoelementach, które akceptują tylko złożone selektory.css ::slotted(:has(.a)) { … } :host(:has(.a)) { … } :host-context(:has(.a)) { … } ::cue(:has(.a)) { … }
  • Ogranicz użycie elementu :has() po pseudoelemencie css ::part(foo):has(:focus) { … }
  • Użycie :visited zawsze będzie miało wartość fałsz.css :has(:visited) { … }

Rzeczywiste dane o skuteczności związane z :has() znajdziesz w Glitch. Dziękujemy Byungwoo za udostępnienie tych informacji i szczegółów dotyczących implementacji.

To wszystko.

Przygotuj się na :has(). Poinformuj o tym znajomych i udostępnij ten post. To zmieni sposób, w jaki podchodzimy do usługi porównywania cen.

Wszystkie wersje demonstracyjne są dostępne w tej kolekcji CodePen.