Od początku istnienia świata (w rozumieniu 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 osiągnęliśmy tego poziomu. 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. Wszystkie prezentacje w tym artykule zawierają informacje o tym, czy są 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. Warunkiem jest posiadanie potomka o klasie 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 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, nad którymi nie ma kursora
css
.grid:has(.grid__item:hover) .grid__item:not(:hover) { ... }
Wybierz kontener zawierający element niestandardowy <todo-list>
css
main:has(todo-list) { ... }
Wybierz wszystkie pojedyncze a
w akapicie, które mają bezpośredni element potomny 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 się porusza, 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 pól 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 w tym przykładzie: spróbuj wpisać prawidłowe i nieprawidłowe wartości oraz włączyć i wyłączyć 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 przepływie dokumentu? Podaje ona pomysły na stylizację typografii wokół 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 wykresy. Jeśli nie mają figcaption
, są widoczne w treści. Gdy jest obecny element figcaption
, zajmuje on całą szerokość i otrzymuje dodatkową margines.
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
. Do aktualizowania odpowiednich atrybutów można użyć JavaScriptu. Gdy aria-expanded
to true
, użyj :has()
, aby wykryć to 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 w unikaniu błędów popełnianych przez użytkowników?
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 zerwania z modelem mentalnym. To okazja do wypróbowania różnych rzeczy. Jednym ze sposobów na przekroczenie granic możliwości 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” w kodzie. 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ńcuchowego stosowania funkcji :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
: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ązanej 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.