CSS - has(), not(), is(), where()
Założenia wstępne
- Znajomość podstawowych zagadnień związanych z CSS-em (specificity, pseudoklasy)
Wprowadzenie
Wiele wiosen temu, gdy zaczynałem swoją frontendową “przygodę”, nie byłem w stanie zrozumieć dlaczego niektórzy developerzy twierdzą, że “CSS jest łatwy i trudny jednocześnie”. Dla mnie stylowanie było przyjemną przerwą od szarpania się z JavaScriptem. Projekty mijały. Wraz z doświadczeniem, zwłaszcza tym przykrym, zrozumiałem, że źle napisany i chaotyczny CSS może wyprodukować więcej siwych włosów niż JavaScript Aglio e Olio. Dobry CSS ma odpowiednią strukturę, porządek, jest przemyślany i wykorzystuje swoje dobrodziejstwa. Dzisiaj chciałbym opisać kilka CSS-owych bajerów, których być może jeszcze nie znasz, a które warto mieć na swoim podorędziu.
has()
W skrócie - jest to pseudoklasa, którą możemy wykorzystać do odwołania się do “parent elementu” (chociaż nie tylko do parent elementu. Więcej na ten temat trochę niżej 👇)
Im szybciej przejdziemy do kodu tym lepiej. Po pierwsze - stworzyłem mały poligon żebyśmy mieli czym się bawić. Zachęcam do korzystania :)
Targetowanie na podstawie elementu
Dodajmy pierwszą regułę wykorzystującą has().
Na samym dole pliku css dodaj poniższy kod:
div:has(p) {
background-color: red;
}
Syntax w podstawowych przykładach jest raczej prosty. Interestujące są "argumenty" przekazane w nawiasach. W powyższym przykładzie dodaliśmy tam p
. W jaki sposób można zinterpretować ten kod? Co on oznacza?
Mówiąc po ludzku - w powyższym przykładzie łapiemy diva, który zawiera w sobie paragraf i zmieniamy jego kolor tła. Ale zaraz… “jego” czyli paragrafu czy może diva ? Odpowiedż brzmi - diva. Szukamy diva (parenta), który spełnia założenia podane w nawiasie pseudoklasy has() i ma odpowiedni kontent. Po dopisaniu tego kodu zauważ, że dwa pudełka (divy) zmieniły kolor tła.
Mówiąc jeszcze w inny sposób - wybraliśmy konkretne divy na podstawie ich zawartości.
W powyższym snippecie do wyboru konkretnego diva użyliśmy elementu p. Czy jesteśmy ograniczeni tylko do używania elementów HTML w argumentach pseudoklasy has()? Oczywiście, że nie. Możemy też w analogiczny sposób skorzystać z klas!
Przejdźmy ponownie do codepena. Zakomentuj poprzednio wklejony kod i na jego miejsce wrzuć poniższy snippet
Targetowanie na podstawie klasy
div:has(.inner-paragraph) {
background-color: red;
}
Myślę, że widzisz już pewien pattern 🙂. W nawiasach pseudoklasy has() skorzystaliśmy z klasy. Dzięki temu jesteśmy w stanie zmodyfikować style diva, który ma w sobie element z klasą "inner-paragraph”.
Targetowanie z wykorzystaniem kombinatorów
div:has(> span) {
background-color: red;
}
Powyższy snippet pokazuje, że jesteśmy w stanie również skorzystać z kombinatorów w celu wyselekcjonowania konkretnego elementu. W powyższym przykładzie łapiemy diva, którego dzieckiem jest span.
Już na tym etapie prawdopodobnie wpadłeś/aś na kilka pomysłów praktycznego zastosowanie tej pseudoklasy. Niejednokrotnie przyjdzie nam zmierzyć się z sytuacją, gdzie będziemy musieli ostylować kilka komponentów, które są praktycznie identyczne ale jeden z nich będzie miał mały detal, który wpłynie na paddingi / layout lub inne kosmetyczne zmiany. Do tej pory, najpopularniejszym rozwiązaniem w takiej sytuacji było dodanie dodatkowej klasy, na podstawie której będziemy w stanie dodać kilka unikalnych modyfikacji. To jedna z sytuacji, gdzie has() może zabłysnąć. Sprawdźmy co jeszcze potrafi!
Targetowanie dziecka
Czy jest ono w ogóle możliwe? Wcześniej powiedzieliśmy sobie, że has() służy do targetowania parent elementu na podstawie zawartości. Coż, jest to prawda ale dzięki temu możemy skorzystać z pewnego tricku.
div:has(svg) > p {
padding: 0;
}
Powyższy snippet pokazuje, że przy pomocy has możemy się też dostać do dziecka danego elementu. Jak to działa?
Po pierwsze - przy pomocy has() targetujemy diva, który w swoim contencie ma ikonkę (w tym przypadku element svg). Skoro mamy już dostęp do parenta, to teraz śmiało możemy skorzystać z kombinatora >
aby dostać się do jego dzieci. Właśnie to robi druga część powyższego selektora.
Bez dopisania żadnej dodatkowej klasy znaleźliśmy parenta na podstawie jego contentu i byliśmy w stanie zmodyfikować style jego dziecka. Całkiem sprytne, prawda ?
"Forgiving" selector list
UPDATE: (16.01.2023)
Lista selektorów przekazywana do has() nie jest już "forgiving". Więcej informacji poniżej
.
Do tej pory każdy podtytuł w tym poście był po polsku. Po 20 minutach zastanawiania się jak przetłumaczyć “forgiving selector” zdecydowałem się to tłumaczenie olać. Może ktoś oświeci mnie w komentarzach 😃
Do rzeczy…
Zazwyczaj w CSS-ie, błąd w selektorze skutkował wyrzuceniem całej reguły do śmieci. has() jest trochę bardziej wyrozumiały. Jeśli popełnimy błąd i przykładowo dopiszemy element, który nie jest wspierany (np. pseudoklasę :after) to błędny fragment zostanie zignorowany a reszta będzie ostylowana zgodnie z naszymi instrukcjami.
Jeśli pierwszy raz spotykasz się z tym zagadnieniem to przeanalizujmy sobie to od początku.
p, span, article:whatever {
background-color: red;
}
W powyższym snippecie chcemy zmienić kolor tła dla zadeklarowanej listy elementów. Lista ta składa się z elementów: p, span oraz article:whatever. Dwa z nich są poprawne ale ten ostatni jest z czapy. Niestety, przez ten jeden niepoprawny element cała reguła zostanie zignorowana.
Tak jak już wspomniałem, sytuacja wygląda inaczej, jeśli korzystamy z has()
div:has(p, span, article:whatever) {
background-color: orangered;
}
Powiedzmy, że szukamy diva, który ma w sobie dany content. Lista argumentów jest identyczna jak w poprzednim snippecie. Pomimo błędu, style zostaną zaaplikowane dla diva, który w swoim contencie zawiera p
oraz span
.
UPDATE: (16.01.2023)
Pod koniec roku 2022 został zgłoszony problem dotyczący has(). Ma on związek z najpotężniejszym frontendowym narzędziem. Oczywiście mowa o React J-Query. Więcej informacji na ten temat można przeczytać w tym wątku github.com/w3c/csswg-drafts/issues/7676. TLDR - najważniejsza informacja jest taka, że lista elementów przekazywana do has(), nie jest już "forgiving". Zmiana ta dotyczy tylko pseudoklasy has().
is()
Kolejna z omawianych dzisiaj pseudo-klas. Przydaje się zwłaszcza w sytuacjach, w których mamy do czynienia z zagnieżdżonymi selektorami. W wielu przypadkach taki selektor staje się niezwykle nieczytelny i nieelegancki. Przejdźmy do przykładu.
Powiedzmy, że chcemy “złapać” wszystkie spany i paragrafy w headerze. Selektor mógłby wyglądać następująco:
header p,
header span {
background-color: red;
}
Przy użyciu is(), jesteśmy w stanie nieco uprościć ten selektor.
header :is(span, p) {
background-color: red;
}
Przyznajmy, że na tym przykładzie nie widać jeszcze wielkiej wartości płynącej z tego rozwiązania. Poniższy przykład może być nieco bardziej przekonujący.
Stylujemy każdy rodzaj nagłówka znajdujący się w elemencie section lub article
section h1, section h2, section h3, section h4, section h5, section h6,
article h1, article h2, article h3, article h4, article h5, article h6 {
color: orangered;
}
Możemy osiągnąć dokładnie taki sam efekt w znacznie krótszej formie dzięki naszej nowej pseudoklasie
:is(section, article) :is(h1, h2, h3, h4, h5, h6) {
color: orangered;
}
Znacznie mniej powtórzeń i poprawiona czytelność - profit 🙂
Warto również zaznaczyć, że podobnie jak w przypadku has() - lista selektorów przekazywana w nawiasach is() jest "forgiving". Działa to na tej samej zasadzie jak w omawianej powyżej pseudoklasie has().
Lista ta może przyjmować dokładnie takie same rodzaje parametrów. Mogą to być elementy html jak a, p span czy div ale również klasy .btn, .header, itd. Użycie kombinatorów również jest dozwolone.
Tak samo jak w przypadku pseudoklasy has(), ograniczeniem jest użycie pseudoelementów w liście argumentów, więc poniższy przykład
div:is( ::after) {
display: block;
}
nie zadziała!
Specyficzność
Słowo kluczowe is() - samo w sobie nie dorzuca żadnych punktów specyficzności, ale argumenty przekazane w nawiasach już tak. Oznacza to, że najmocniejszy zawodnik z nawiasów będzie dorzucał swoje punkty specificity do ogólnych punktów całej reguły.
.section :is(.title, p) { // Wyższa specyficzność przez klasę .title
color: orangered;
}
.section p {
color: blue;
}
W powyższym snippecie wyższą specyficzność ma pierwsza reguła. Na jej moc składają się 2 elementy:
- .section
- .title (najsilniejszy zawodnik w grupie przekazanej w nawiasach)
W przypadku drugiej reguły:
- .section
- p
Połącznie has() i is()
Pseudoklasy has() oraz is() możemy łączyć. Oto dowód:
:is(article, div, section):has(+ :is(h2, h3, h4)) {
margin: 0.5rem;
}
W celu zrozumienia tej reguły, skupmy się najpierw na pseudoklasach is(). Pierwsza z nich pozwala nam wybrać elementy div, article i section. Dzięki drugiemu is() jesteśmy w stanie przeszukać dzieci w poszukiwaniu elementow h2, h3 i h4. Dzięki pseudoklasie has() wybieramy takie article, div oraz section, ktorych bezpośrednimi dziećmi są nagłowki typu h2, h3 lub h4.
where() (i czym się różni od is())
W celu zrozumienia pseudoklay where() najlepiej przescrollować trochę wyżej, do poprzedniego nagłówka. Dokładnie tego, w którym opisuję psueudoklasę is(), i przeczytać całość jeszcze raz. Nie, nie mam wylewu. Ich działanie jest prawie identycznie. Jedyną znaczącą różnicą, o której trzeba pamiętać jest specyficzność (specificity). W przypadku where() warto pamiętać, że, ze specificity elementów z nawiasów nie przyczynia się w żaden sposób do siły reguły.
not()
Dzięki not() możemy wykluczyć dany rodzaj elementu z naszego obszaru poszukiwań. Najlepiej pokazać jego działanie na przykładach.
/*Elementy p, które nie mają klasy active*/
p:not(.active) {
color: red;
}
/*Elementy które nie są paragrafami*/
div :not(p) {
text-decoration: none;
}
/*Elementy, które nie są paragrafami i nie są divami*/
body :not(p):not(div) {
background-color: teal;
}
/*Dzieci div-a, które nie są paragrafami i nie mają klasy active*/
div :not(p, .active) {
display: none;
}
/*Elementy wewnątrz nawigacji, które nie są buttonami o klasie active*/
nav :not(button.active) {
display: none;
}
Not() ma kilka mankamentów i pułapek. Prawdopodobieństwo napotkania którejś z nich raczej jest niskie ale warto chociaż raz przeczytać listę dostępną na MDN.
Podsumowanie
Powyższe pseudoklasy wprawdzie nie są czymś absolutnie przełomowym ale zdecydowanie pozytywnie wpłyną na czytelność arkuszy styli. Zwłaszcza tych zbyt skomplikowanych i składających się z mnóstwa zagnieżdżeń. Pisanie kodu DRY zawsze powinno być z tyłu naszej głowy i znajomość powyższych trików znacznie nam w tym pomoże.
Jak już wspomniałem, utrzymanie źle napisanego CSS-a bywa koszmarem. Postarajmy się do tego nie dopuścić ;) Niech powyższe narzędzia będą krokiem w dobrą stronę :)