:has(): selektor rodziny

Od samego początku (w kontekście CSS) pracujemy nad kaskadą w różnych sensach. Nasze style tworzą „Kaskadowy arkusz stylów”. Również nasze selektory spełniają ruch kaskadowy. Mogą przejść bokiem. W większości przypadków znikają w dół. Ale nigdy wyżej. Od lat marzymy o funkcji wyboru rodziców. I teraz w końcu nadchodzi! W kształcie pseudoselektora :has().

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

To coś więcej niż tylko selektor „nadrzędny”. To dobry sposób na jego promocję. Niezbyt atrakcyjnym sposobem może być użycie selektora „środowiska warunkowego”. Ale to nie ma takiego samego pierścienia. A co z selektorem „rodzina”?

Obsługa przeglądarek

Zanim przejdziemy dalej, wspomnimy o obsłudze przeglądarek. Jeszcze jej nie ma. Ale jest coraz bliżej. Firefox nie jest jeszcze obsługiwany, ale planujemy go włączyć. Jest on już dostępny w przeglądarce Safari i zostanie udostępniony w Chromium 105. Wszystkie wersje demonstracyjne dostępne w tym artykule pozwolą Ci sprawdzić, czy Twoja przeglądarka nie obsługuje ich.

Jak używać :has

Jak to wygląda? Weźmy pod uwagę podany niżej kod HTML z 2 elementami równorzędnymi o klasie everybody. Jak wybierzesz to, które ma element podrzędny o klasie a-good-time?

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

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

:has() umożliwia to za pomocą poniższego kodu CSS.

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

Spowoduje to wybranie pierwszego wystąpienia funkcji .everybody i zastosowanie reguły animation.

W tym przykładzie celem jest element o klasie everybody. Warunek ma element podrzędny o klasie a-good-time.

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

Możesz jednak pójść o wiele dalej, ponieważ :has() otwiera przed Tobą wiele możliwości. Nawet takie, które prawdopodobnie jeszcze nie zostały odkryte. Weź pod uwagę niektóre z nich.

Wybierz elementy figure, które mają bezpośrednie połączenie figcaption. css figure:has(> figcaption) { ... } Wybierz elementy anchor, które nie mają bezpośredniego elementu potomnego SVG css a:not(:has(> svg)) { ... } Wybierz obiekty label, które bezpośrednio mają element równorzędny input. Obrócone! css label:has(+ input) { … } Wybierz elementy article, przy których element podrzędny img nie ma tekstu alt. css article:has(img:not([alt])) { … } Wybierz element documentElement, w którym w DOM css :root:has(.menu-toggle[aria-pressed=”true”]) { … } Wybierz kontener układu z nieparzystą liczbą elementów podrzędnych css .container:has(> .container__item:last-of-type:nth-of-type(odd)) { ... } Wybierz wszystkie elementy siatki, które nie są najeżdżane kursorem css .grid:has(.grid__item:hover) .grid__item:not(:hover) { ... } Wybierz kontener zawierający element niestandardowy <todo-list> css main:has(todo-list) { ... } Wybierz element documentElement, w którym określony element: a{11/Condition} w akapicie {11/Condition}, który występuje w akapicie.articlehrcss p:has(+ hr) a:only-child { … }css article:has(>h1):has(>h2) { … } Wybierz element article, po którym następuje tytuł css article:has(> h1 + h2) { … } Wybierz :root po wywołaniu stanów interaktywnych css :root:has(a:hover) { … } Wybierz akapit występujący po elemencie figure, który nie ma figcaption css figure:not(:has(figcaption)) + p { … }

Jakie ciekawe przypadki użycia przydają się w przypadku :has()? Fascynujące w tym filmie jest to, że zachęca Cię do łamania modelu myślenia. Skłania to do zastanowienia się: „Czy mogę podejść do tych stylów w inny sposób?”.

Przykłady

Przyjrzyjmy się kilku przykładom, jak można go wykorzystać.

Karty

Zobacz klasyczną prezentację kart. Na naszej karcie możemy wyświetlać dowolne informacje, np. tytuł, podtytuł lub multimedia. Oto podstawowa karta.

<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 przedstawić kilka treści multimedialnych? W przypadku tego projektu karta może zostać podzielona na 2 kolumny. Wcześniej można było utworzyć nową klasę do reprezentowania tego działania, na przykład card--with-media lub card--two-columns. Takie nazwy zajęć nie tylko trudno jest wymyślić, ale także trudno jest je zapamiętać i zapomnieć.

Za pomocą :has() możesz wykryć, że na karcie są jakieś multimedia, i wykonać odpowiednie czynności. Nie ma potrzeby podawania 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 jej tam zostawiać. Możesz puścić wodze fantazji. Jak karta z polecanymi treściami może dostosować się do układu strony? Ten kod CSS ustawi polecaną kartę o pełnej szerokości układu i umieści ją 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 się stanie, jeśli wyróżniona karta z banerem zacznie się ruszać, aby przyciągnąć 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 co z formularzami? Słyną z tego, że trudno nadać im styl. Przykładem może być styl danych wejściowych i ich etykiety. Jak zasygnalizować na przykład, że dane pole jest prawidłowe? Dzięki :has() jest to dużo łatwiejsze. Możemy dołączyć do odpowiednich pseudoklas (np. :valid i :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);
}

Przyjrzyj się temu przykładowi. Spróbuj wpisać prawidłowe i nieprawidłowe wartości, odznaczając się odpowiednimi informacjami.

Możesz też użyć polecenia :has(), aby wyświetlić lub ukryć komunikat o błędzie w danym polu. Wypełnij 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 zmieni się w :invalid i nie będzie zaznaczone, możesz wyświetlić wiadomość bez konieczności podawania dodatkowych nazw klas.

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

Nie ma powodu, dla którego nie możesz dodać zabawnej porcji zabawnych treści, które skłonią użytkowników do interakcji z formularzem. Przeanalizuj ten przykład. Uważaj, gdy wpiszesz prawidłową wartość mikrointerakcji. Wartość :invalid będzie powodować drżenie grupy formularzy. Jednak tylko wtedy, gdy użytkownik nie ma preferencji dotyczących ruchu.

treści

Omówiliśmy to w przykładach kodu. Jak jednak wykorzystać :has() w przepływie dokumentów? Podpowiada to na przykład, jak możemy zmienić styl typografii w mediach.

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 liczby. Gdy nie mają elementu figcaption, pływają w obrębie treści. Gdy występuje figcaption, zajmują one pełną szerokość i otrzymują dodatkowy margines.

Reakcja na stan

A może chcesz, aby Twoje style były reaktywne dla jakiegoś stanu w naszych znacznikach. Przeanalizujmy przykład z „klasycznym” przesuwnym paskiem nawigacyjnym. Jeśli masz przycisk, który służy do otwierania nawigacji, możesz użyć atrybutu aria-expanded. Do aktualizacji odpowiednich atrybutów można użyć JavaScriptu. Gdy aria-expanded ma wartość true, użyj :has(), aby to wykryć i zaktualizować style przesuwanego panelu nawigacyjnego. JavaScript wykonuje swoją część, a CSS potrafi z tymi informacjami to, co chce. Nie ma potrzeby tasowania znaczników ani dodawania dodatkowych nazw klas itd. (Uwaga: przykład nie jest gotowy do wykorzystania w środowisku produkcyjnym).

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

Czy :has pomaga uniknąć błędu użytkownika?

Co łączy te wszystkie przykłady? Poza tym, że pokazują sposoby używania :has(), żaden z nich nie wymagał zmiany nazw klas. Każdy z nich wstawił nową treść i zaktualizował atrybut. To duża zaleta :has(), ponieważ pomaga ograniczyć liczbę błędów popełnianych przez użytkowników. Dzięki :has() CSS może przejąć odpowiedzialność za dostosowanie się do zmian w DOM. Nie musisz żonglować nazwami klas w języku JavaScript, co zmniejsza ryzyko popełnienia błędu. Zdarza się, że gdy pomylimy nazwę zajęć w nazwie zajęć, musimy je zachować w wynikach wyszukiwania w usłudze Object.

To ciekawa myśl i czy prowadzi do wprowadzenia bardziej przejrzystych znaczników i mniej kodu? Mniej JavaScriptu, ponieważ nie wykonujemy tylu korekt tego kodu. Mniej kodu HTML, ponieważ nie potrzebujesz już klas takich jak card card--has-media itp.

Nieszablonowe myślenie

Jak już wspomnieliśmy, :has() zachęca do łamania modelu myślenia. To okazja, by spróbować różnych rzeczy. Jednym ze sposobów na przekroczenie granic jest opracowanie mechaniki gry za pomocą samego CSS. Można to zrobić na przykład 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;
}

A to otwiera ciekawe możliwości. Możesz użyć tej funkcji do przemierzania formy z przekształceniami. Uwaga: tę prezentację najlepiej wyświetlić na osobnej karcie przeglądarki.

A co z klasyczną grą przewodową? Mechanika treści w :has() jest łatwiejsza do utworzenia. Jeśli przewód się najedzie na przewód, będzie to oznaczać, że gra się zakończyła. Tak, niektóre z mechanizmów gier można stworzyć, używając na przykład kombinatorów rodzeństwa (+ i ~). :has() to jednak sposób na osiągnięcie tych samych wyników bez konieczności używania ciekawych sztuczek znaczników. Uwaga: tę prezentację najlepiej wyświetlić na osobnej karcie przeglądarki.

W najbliższym czasie nie udostępnimy tych elementów w wersji produkcyjnej, ale przedstawimy sposoby wykorzystania elementów podstawowych. Na przykład możliwość połączenia elementu :has() w łańcuchu.

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

Wydajność i ograniczenia

Zanim skończymy, czego nie możesz robić w aplikacji :has()? W usłudze :has() obowiązują pewne ograniczenia. Główne z nich powstają w wyniku uderzeń związanych z wydajnością.

  • Nie możesz użyć polecenia :has(), aby :has(). Możesz jednak połączyć :has() w łańcuch. css :has(.a:has(.b)) { … }
  • Brak użycia pseudoelementów w obrębie :has() css :has(::after) { … } :has(::first-letter) { … }
  • Ogranicz używanie elementu :has() w pseudos, akceptujących tylko selektory złożone css ::slotted(:has(.a)) { … } :host(:has(.a)) { … } :host-context(:has(.a)) { … } ::cue(:has(.a)) { … }
  • Ogranicz użycie :has() po pseudoelementze css ::part(foo):has(:focus) { … }
  • Użycie właściwości :visited będzie zawsze miało wartość fałsz. css :has(:visited) { … }

Rzeczywiste dane o skuteczności reklam :has() znajdziesz w tym artykule. Podziękowania dla firmy Byungwoo za podzielenie się spostrzeżeniami i szczegółami dotyczącymi wdrożenia.

To wszystko.

Przygotuj się na :has(). Powiedz o tym znajomym i udostępnij tego posta. Może on zmieni nasze podejście do CSS.

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