Złożony obraz młodego mężczyzny pokazujący ekran jego komputera typu tablet

Pseudoklasy :is() i :where() w CSS – jak upraszczają selektory

12 min. czytania

Pseudoklasy :is() i :where() to jedne z najciekawszych nowości w CSS Selectors Level 4, bo pozwalają radykalnie skrócić złożone selektory, ograniczyć kopiuj‑wklej i lepiej panować nad specyficznością – czyli tym, jak łatwo (lub trudno) nadpisać daną regułę. Dzięki nim CSS staje się czytelniejszy, łatwiejszy w utrzymaniu, a spójne stany interakcji (np. focus) są prostsze do wdrożenia i utrzymania, co realnie wspiera dostępność.

Krótkie przypomnienie – pseudoklasy i specyficzność

Pseudoklasy to specjalny rodzaj selektorów prostych, dzięki którym wybieramy elementy na podstawie ich stanu lub innych cech, których nie widać w samym drzewie DOM (np. :hover, :focus, :first-child). Zawsze poprzedza je dwukropek, np. a:hover.

Specyficzność (ang. specificity) określa, jak “silny” jest selektor – czyli który styl wygra, gdy kilka reguł pasuje do tego samego elementu.

W uproszczeniu:

  • selektory ID są bardziej specyficzne niż klasy,
  • klasy / pseudoklasy / atrybuty są bardziej specyficzne niż selektory tagów,
  • im wyższa specyficzność, tym trudniej później nadpisać regułę.

:is() i :where() działają jak “funkcje” w selektorach – przyjmują listę selektorów i dopasowują elementy pasujące do któregokolwiek z nich, ale różnią się tym, jak wpływają na specyficzność.

Co robi pseudoklasa :is()

Definicja i dopasowywanie

:is() (wcześniej znane jako :matches()) to funkcjonalna pseudoklasa, która przyjmuje listę selektorów rozdzielonych przecinkami i dopasowuje elementy pasujące do dowolnego z nich.

Ogólny wzór:

:is(selektor1, selektor2, selektor3) { … }

Każdy element, który spełni którykolwiek z selektorów w nawiasach, zostanie ostylowany tą regułą. Możemy jej używać w dowolnym miejscu selektora – na początku, w środku, na końcu oraz zagnieżdżać ją w innych :is() / :where().

Podstawowy przykład – skracanie długich selektorów

Załóżmy, że w menu nawigacji chcemy ostylować linki niezależnie od tego, czy są:

  • zwykłym linkiem,
  • przyciskiem,
  • elementem o roli linku.

Bez :is():

nav a, nav button, nav [role="link"] { font-weight: 600; text-decoration: none; }

Z :is():

nav :is(a, button, [role="link"]) { font-weight: 600; text-decoration: none; }

Jeden czytelny selektor zamiast trzech prawie identycznych – to dokładnie ten typ problemu, który :is() ma rozwiązać.

Grupowanie stanów interakcji

Bardzo częsty wzorzec:

button:hover, button:focus-visible, a.button:hover, a.button:focus-visible { outline: 2px solid currentColor; }

Z :is():

:is(button, a.button):is(:hover, :focus-visible) { outline: 2px solid currentColor; }

W jednym miejscu grupujemy typ elementu (button albo a.button) oraz stany interakcji (:hover, :focus-visible).

To nie tylko mniej kodu – trudniej też zapomnieć o którymś stanie, co jest kluczowe dla dostępności.

“Forgiving selector list” – odporność na błędne selektory

:is() przyjmuje tzw. wybaczającą listę selektorów (forgiving selector list). Oznacza to, że jeśli jakiś selektor w nawiasach jest nieprawidłowy lub nieobsługiwany, jest ignorowany, ale reszta listy działa normalnie:

:is(header, main, footer, nav:has(a)) { padding-inline: 1.5rem; }

Jeśli przeglądarka nie obsługuje :has(), ten fragment zostanie pominięty z listy, ale style nadal zadziałają dla header, main i footer.

Gdybyśmy napisali to klasycznym grupowaniem:

header, main, footer, nav:has(a) { … }

i :has() byłby nieobsługiwany, cała reguła zostałaby uznana za niepoprawną i odrzucona. To czyni :is() (i :where()) świetnym narzędziem do progressive enhancement.

Specyficzność :is()

Najważniejsza cecha :is():

Specyficzność :is() jest równa specyficzności najbardziej specyficznego selektora w nawiasach i wlicza się do specyficzności całego selektora.

Przykład:

article :is(h1, h2, h3) { color: red; }

h1, h2, h3 to selektory tagów, więc specyficzność listy jest jak dla typu elementu. Cały selektor ma zatem specyficzność zsumowaną z article + :is(h1, h2, h3) (czyli nadal dość niską).

Bardziej wyrazisty przykład:

article :is(h1.title, h2.title) { color: red; }

W nawiasach mamy selektory zawierające klasę i tag, więc specyficzność rośnie – i właśnie ten najwyższy poziom specyficzności przechodzi na całą pseudoklasę :is().

Wniosek: :is() może podnieść specyficzność selektora (w porównaniu do prostego article h1), dlatego warto ostrożnie dobierać zawartość nawiasów, aby nie stworzyć trudnego do nadpisania “potwora”.

Co robi pseudoklasa :where()

Ten sam mechanizm dopasowywania

Z punktu widzenia dopasowywania elementów :where() działa identycznie jak :is() – przyjmuje listę selektorów rozdzielonych przecinkami, pasuje do elementów spełniających którykolwiek z nich, można jej używać w dowolnym miejscu selektora i przyjmuje wybaczającą listę selektorów (błędne są ignorowane):

nav :is(a, button, [role="link"]) { … } nav :where(a, button, [role="link"]) { … }

Różnica kryje się w specyficzności.

Specyficzność :where() – zawsze 0

Najważniejsza cecha :where():

Selektory w nawiasach :where() nie wnoszą żadnej specyficzności – specyficzność pseudoklasy :where() jest zawsze równa 0.

To pierwsza funkcja selektora w CSS, która w ten sposób “anuluje” specyficzność listy selektorów. Przykład porównawczy:

article :is(h1.title, h2.title) { color: red; } article :where(h1.title, h2.title) { color: blue; }

W pierwszej regule (:is()) specyficzność jest wyższa (pochodzi z klasy .title), a w drugiej (:where()) niższa, bo cały fragment w nawiasach nie liczy się do specyficzności; liczy się tylko article. Efekt: reguła z :is() będzie trudniejsza do nadpisania, a reguła z :where() – bardzo łatwa.

To sprawia, że :where() idealnie nadaje się do:

  • stylów bazowych i resetów, które chcesz łatwo nadpisywać w komponentach,
  • globalnych wzorców (np. marginesy nagłówków, padding w formularzach),
  • wszystkiego, co świadomie ma mieć jak najniższą specyficzność.

Przykład – reset nagłówków z niską specyficznością

Zamiast klasycznego grupowania:

h1, h2, h3, h4, h5, h6 { margin: 0 0 .5em; }

można napisać:

:where(h1, h2, h3, h4, h5, h6) { margin: 0 0 .5em; }

Oba selektory dopasują te same elementy, ale druga reguła będzie dużo łatwiejsza do nadpisania, bo ma specyficzność 0. Dzięki temu komponenty (np. “hero”) lub style użytkownika mają większą szansę wygrać – co jest ważne dla dostępności.

Przykład – bazowe style pól formularza

Globalne ujednolicenie wyglądu pól formularza:

form :where(input, select, textarea, button) { font: inherit; color: inherit; }

Wszystkie podstawowe kontrolki dziedziczą typografię, ale pojedyncze komponenty (np. przycisk typu “primary”) łatwo je nadpiszą, bo specyficzność :where() jest zerowa.

:is() vs :where() – podobieństwa i różnice

Podobieństwa

Obie pseudoklasy łączy kilka wspólnych cech:

  • ten sam sposób dopasowywania – lista selektorów, element pasuje jeśli spełnia którykolwiek z nich,
  • ta sama składnia – nazwa pseudoklasy + nawiasy z listą selektorów,
  • ta sama wybaczająca lista selektorów – błędne selektory są ignorowane, reszta działa,
  • można je zagnieżdżać i łączyć, np. :is(header, main) :where(a, button).

Kluczowa różnica – specyficzność

Porównanie najważniejszych aspektów przedstawia poniższa tabela:

Aspekt :is() :where()
Dopasowywanie element spełnia którykolwiek selektor z listy to samo
Składnia identyczna (:is(selektor1, selektor2, …)) identyczna (:where(selektor1, selektor2, …))
Specyficzność równa najbardziej specyficznemu selektorowi w nawiasach zawsze 0, lista nie podnosi specyficzności
Główne zastosowanie gdy chcesz uprościć selektory, nie tracąc siły dopasowania gdy chcesz uprościć selektory i celowo zachować niską specyficzność
Ryzyko “walk specyficzności” łatwo przypadkiem je zwiększyć praktycznie brak – reguły łatwo nadpisać

Krótko: :is() stosuj, gdy chcesz usunąć powtórzenia i zarazem utrzymać lub wzmocnić “siłę” selektora; :where() wybierz, gdy chcesz świadomie “osłabić” regułę, by była łatwa do nadpisania.

Praktyczne wzorce: jak :is() i :where() upraszczają selektory

DRY w komponentach – jeden selektor zamiast wielu

Przykład: przyciski w różnych wariantach

Bez :is():

.button-primary, .button-secondary, .button-tertiary { border-radius: 9999px; padding-inline: 1.25rem; }

Z :is():

:is(.button-primary, .button-secondary, .button-tertiary) { border-radius: 9999px; padding-inline: 1.25rem; }

Mniej powtórzeń, jedno źródło prawdy – grupujemy wspólne cechy i minimalizujemy ryzyko rozbieżności.

Dla stanów interakcji:

Bez :is():

.button-primary:hover, .button-primary:focus-visible, .button-secondary:hover, .button-secondary:focus-visible { box-shadow: 0 0 0 3px color-mix(in srgb, currentColor 30%, transparent); }

Z :is():

:is(.button-primary, .button-secondary):is(:hover, :focus-visible) { box-shadow: 0 0 0 3px color-mix(in srgb, currentColor 30%, transparent); }

:is() eliminuje podwójne powtarzanie tego samego schematu stanów.

Lepsze, spójne stany focus/hover (dostępność)

Użytkownicy korzystają z różnych metod nawigacji:

  • mysz / touchpad – stan :hover,
  • klawiatura – stan :focus / :focus-visible,
  • urządzenia asystujące – często polegają na dobrze widocznym focus.

Łatwo o błąd: poprawiamy :hover, a zapominamy o :focus-visible. Z :is() możemy od razu grupować stany w jednym miejscu:

:is(a, button, [role="button"]):is(:hover, :focus-visible) { outline: 2px solid currentColor; outline-offset: 2px; }

Taki zapis:

  • zapewnia spójność wizualną między różnymi metodami wejścia,
  • ułatwia egzekwowanie wytycznych WCAG dotyczących widocznego focusu,
  • zmniejsza ryzyko, że jakiś stan zostanie pominięty przy refaktorze.

Globalne “ramy” layoutu z :where()

Przykład: sekcje strony

Zamiast:

header, main, footer, aside, section { padding-inline: 1.5rem; }

możemy napisać:

:where(header, main, footer, aside, section) { padding-inline: 1.5rem; }

Zyskujemy jedno miejsce, gdzie opisujemy “ramy” layoutu oraz niską specyficzność, którą bez problemu nadpiszą style komponentów.

“Odchudzanie” BEM‑owych selektorów

W kodzie BEM częste są konstrukcje:

.card__title, .card__subtitle, .card__meta { margin-bottom: .5rem; }

Z :is():

.card :is(.card__title, .card__subtitle, .card__meta) { margin-bottom: .5rem; }

Mniej powtórzeń i bardzo jasny kontekst (.card) na początku selektora.

A jeśli to mają być jedynie domyślne odstępy, które komponenty mogą zmienić, lepsze będzie:

.card :where(.card__title, .card__subtitle, .card__meta) { margin-bottom: .5rem; }

Świadomie obniżamy specyficzność.

Kontrola specyficzności w praktyce

Kiedy użyć :is(), a kiedy :where()

Użyj :is(), gdy:

  • chcesz skrócić selektor i jednocześnie zachować lub podbić jego specyficzność,
  • tworzysz regułę, która ma być dość “mocna”, np. nadrzędne style komponentu,
  • i tak już operujesz na klasach / ID, więc specyficzność jest świadomie wysoka.

Użyj :where(), gdy:

  • piszesz globalne style bazowe (typografia, reset list, marginesy nagłówków),
  • chcesz zachować styl, ale nie wygrywać “wojen o specyficzność” z komponentami,
  • zależy ci, by style użytkownika / przeglądarki miały szansę nadpisać twoje reguły – istotne dla dostępności (np. tryby wysokiego kontrastu, własne arkusze użytkownika).

Przykład – karta artykułu z bazowymi stylami

Bazowe typograficzne style artykułów:

.article :where(h1, h2, h3, h4, h5, h6) { font-family: system-ui, sans-serif; margin: 0 0 .75em; }

Specyficzność jest niska, więc wewnątrz specjalnego komponentu – np. hero – można bez problemu wprowadzić inny wygląd nagłówka:

.article-hero h1 { font-size: clamp(2.5rem, 4vw, 3.5rem); margin-bottom: 1rem; }

Nie potrzebujemy !important i nie walczymy z kaskadą – to efekt przemyślanego użycia :where().

:is() i :where() a dostępność

Choć specyfikacja CSS nie jest bezpośrednio “o dostępności”, sposób, w jaki organizujemy style, ma realny wpływ na doświadczenie użytkowników z niepełnosprawnościami.

Spójne stany interakcji

Dzięki :is() i :where():

  • łatwiej zapewnić spójny hover/focus/active dla całych grup elementów,
  • łatwiej zarządzać kontrastem i widocznością focusu,
  • zmniejsza się ryzyko, że jakiś wariant będzie miał gorszy focus tylko dlatego, że został pominięty w osobnej regule.

Przykład:

main :is(a, button, [role="button"]):is(:hover, :focus-visible) { outline: 2px solid currentColor; outline-offset: 2px; }

Ten selektor jednym ruchem obejmuje wszystkie główne interaktywne elementy w treści – przydatne na stronach z dużą liczbą linków.

Poszanowanie stylów użytkownika

Stosowanie :where() do bazowych stylów o zerowej specyficzności wspiera:

  • arkusze użytkownika (user stylesheets),
  • tryby wysokiego kontrastu i inne ustawienia systemowe,
  • domyślne style UA (User Agent), tam gdzie powinny mieć pierwszeństwo.

Gdy globalne style mają niską specyficzność, użytkownik ma większe szanse dostosować wygląd do własnych potrzeb – zgodnie z duchem WCAG i zasadami projektowania inkluzywnego.

Łatwiejsze utrzymanie – mniej regresji

Kod z mniejszą liczbą powtórzeń:

  • jest prostszy do refaktora,
  • lepiej przechodzi przeglądy dostępności (łatwiej prześledzić, gdzie zmieniamy focus/kontrast),
  • ogranicza “tajemnicze” regresje, gdy modyfikujemy jeden wariant przycisku, a inny zostaje pominięty.

:is() i :where() promują deklaratywne myślenie o grupach elementów, co zwykle przekłada się na stabilniejsze style i mniej błędów wpływających na użytkowników wrażliwych na zmianę kontrastu, brak focusu czy migotanie.

Wsparcie przeglądarek i strategia użycia

Pseudoklasy :is() i :where() są częścią specyfikacji Selectors Level 4 i są już wspierane przez wszystkie nowoczesne przeglądarki (Chrome, Firefox, Safari, Edge w aktualnych wersjach). Starsze przeglądarki, które ich nie implementują (np. Internet Explorer), po prostu ignorują reguły z :is() / :where().

Praktyczna strategia:

  • w nowych projektach, nastawionych na współczesne przeglądarki – używaj :is() i :where() śmiało,
  • jeśli musisz wspierać starą przeglądarkę – stosuj progressive enhancement,
  • kluczowe style (np. widoczne focusy, kontrast) możesz zdefiniować klasycznymi selektorami,
  • a :is() / :where() wykorzystać do udoskonalania i redukcji duplikacji w nowoczesnych przeglądarkach.

W połączeniu z ich “wybaczającą” naturą nadają się też dobrze do stopniowego wdrażania innych nowych selektorów, takich jak :has().

Kilka praktycznych wzorców do skopiowania

Bazowe marginesy tekstu

:where(p, ul, ol, blockquote, figure) { margin-block: 0 1rem; }

Spójne style linków w treści

main :where(a) { text-decoration-thickness: .1em; text-underline-offset: .2em; }

Stany interakcji dla wszystkich przycisków i linków‑przycisków

:is(button, input[type="button"], input[type="submit"], a[role="button"]):is(:hover, :focus-visible) { filter: brightness(1.05); }

Uproszczone selektory kart

.card :is(h2, h3) { font-size: 1.25rem; margin-bottom: .5rem; } .card :where(p, ul, ol) { margin-bottom: .75rem; }