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
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);
}
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 (+
i ~
). 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 pseudoelemenciecss ::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.