Piszesz .sidebar .card { color: blue; }, ładujesz stronę, a karta jest wciąż czerwona przez regułę dodaną godzinę wcześniej: .card { color: red; }. Nowa reguła jest niżej w pliku. I mimo to przegrywa. Przeglądarka porównuje dwa selektory przy użyciu stałego systemu punktowego, a .sidebar .card dostaje wyższy wynik niż .card, niezależnie od tego, który napisano później.
Ten system punktowy to specyficzność. Kiedy już umiesz ją liczyć od razu, “czemu mój CSS się nie stosuje” staje się arytmetyką. Nie ma to nic wspólnego z layoutem: Grid czy Flexbox decyduje, gdzie znajduje się element, a specyficzność decyduje, która reguła ustawia jego kolor albo rozmiar, gdy dwie reguły są w konflikcie.
Jak przeglądarka ocenia selektor
Każdy selektor ma wagę złożoną z trzech liczb, zwykle zapisywaną jako trójka oddzielona myślnikami: ID-klasa-typ. Zlicz, co zawiera selektor, kolumna po kolumnie:
| Kolumna | Co się liczy | Waga za wystąpienie |
|---|---|---|
| ID | #header | 1-0-0 |
| Klasa / atrybut / pseudoklasa | .card, [type="radio"], :hover, :nth-child(2) | 0-1-0 |
| Typ / pseudoelement | div, h1, ::before | 0-0-1 |
Selektor uniwersalny * i kombinatory (spacja, >, ~, +) dodają 0-0-0. Decydują, które elementy pasują, a nie jak silnie wygrywa reguła.
Kilka realnych selektorów:
| |
Aby porównać dwie wartości, czyta się je od lewej do prawej i zatrzymuje na pierwszej kolumnie, która się różni. 1-1-0 wygrywa z 0-2-0, bo kolumna ID (1 wobec 0) decyduje, zanim ktokolwiek spojrzy na klasy. Dlatego #main .card wygrywa z .sidebar .card .title .text, niezależnie od tego, jak długi jest drugi selektor. Jedno ID przebija każdą liczbę klas.
Czemu nowsza reguła CSS może i tak przegrać
Wróćmy do przykładu z początku, z wyliczonymi liczbami:
| |
0-2-0 wygrywa z 0-1-0 w kolumnie klas, więc karta jest niebieska niezależnie od kolejności w pliku. Kolejność w pliku to kryterium, po które sięga prawie każdy jako pierwsze, ale działa tylko wtedy, gdy trójki są identyczne:
| |
Obie reguły mają wynik 0-1-0, więc wygrywa ta, która jest niżej w arkuszu stylów (albo w późniejszym bloku <style>, albo w arkuszu podlinkowanym po drugim). To jedyny przypadek, w którym “wygrywa ostatnia” jest właściwym modelem. Działa to w małych projektach, dlatego każdy uczy się tego najpierw, i przestaje działać, gdy projekt ma więcej niż kilka selektorów.
Style inline stoją nad tym wszystkim. Atrybut style="color: green" na elemencie wygrywa z każdą regułą w każdym arkuszu stylów, niezależnie od liczby ID, które ta reguła łączy. Tylko deklaracje !important mogą go pobić.
| |
| |
!important nie jest częścią trójki specyficzności. To osobny, wyższy priorytet: każda deklaracja !important wygrywa z każdą normalną deklaracją, stylami inline włącznie, niezależnie od wagi selektora. Gdy dwie deklaracje !important się zderzają, decyduje specyficzność (potem kolejność w pliku), tak samo jak bez flagi. Tak jeden !important zmienia się w wyścig zbrojeń. Następny deweloper, który musi go nadpisać, nie ma innego ruchu niż kolejny !important, i szybko nikt już nie wie, która reguła ma wygrywać.
Które selektory dodają zero specyficzności: :where() a :is()
Kilka selektorów pozwala celować precyzyjnie bez płacenia normalnej ceny.
:where() zawsze wnosi 0-0-0, niezależnie od tego, co w nim umieścisz:
| |
To sprawia, że :where() jest właściwym wrapperem dla wartości domyślnych i stylów motywu, które muszą być łatwe do nadpisania. Biblioteka komponentów może dostarczać swoje domyślne style w :where(), a zwykła reguła .card { } napisana przez osobę korzystającą z biblioteki pobije je bez potrzeby !important.
:is(), :not() i :has() działają inaczej. Nie dodają własnej wagi, ale przejmują wagę swojego najbardziej specyficznego argumentu:
| |
Więc :where() jest darmowe, a :is() kosztuje tyle, co najcięższa rzecz, którą w nim umieścisz, nawet jeśli ten argument nigdy nie dopasuje się do elementu.
Jak cascade layers nadpisują specyficzność
Jeśli piętrzysz klasy i ID, by wygrywać starcia, które nie mają nic wspólnego z tym, jak specyficzny powinien być selektor, projekt potrzebuje @layer, a nie więcej zagnieżdżeń.
| |
Kolejność layerów wynika z miejsca, w którym deklarujesz nazwy, a nie z wagi selektorów. utilities zadeklarowano jako ostatni, więc reguła w nim wygrywa z regułą w components, nawet jeśli .text-blue ma tę samą wagę 0-1-0 co .card. Specyficzność wciąż rozstrzyga konflikty wewnątrz layera. Między layerami kolejność layerów wygrywa bez odwołania, a specyficzność nigdy nie jest sprawdzana.
Jedna reguła łapie prawie każdego przy pierwszym razie: dla normalnych deklaracji (bez !important) style poza każdym layerem wygrywają ze stylami wewnątrz layera. Przenosisz CSS komponentów do @layer components, ale zostawiasz jedno nadpisanie bez layera, i ta reguła bez layera wygrywa niezależnie od kolejności, w jakiej zadeklarowałeś layery. Dla deklaracji !important priorytet się odwraca: reguły important z layera zadeklarowanego jako pierwszy wygrywają z tymi z późniejszych layerów, a niepoukładane w layery style important lądują na samym dole wszystkich. Odwrócenie jest zamierzone, i to ta część specyfikacji, którą warto przeczytać ponownie, zanim na niej polegasz.
Wsparcie przeglądarek przestało być argumentem przeciw layerom już jakiś czas temu: @layer działa w każdym głównym silniku od 2022 roku. Prawdziwy koszt jest organizacyjny. Zespół musi wcześniej ustalić kolejność layerów (reset, base, components, utilities to częsty schemat) i wkładać nowy CSS do właściwego layera. To bardziej problem procesu niż składni.
Częste błędy ze specyficznością CSS
- Walka ze specyficznością większą specyficznością. Każde nadpisanie dodaje jedną klasę lub ID więcej niż poprzednie, aż zmiana koloru wymaga
#app .page .section .card.is-active.is-selected. To ten sam wzorzec awarii co spaghetti code: każda poprawka dodaje więcej splotu, zamiast go usuwać. Rozwiązaniem prawie nigdy nie jest cięższy selektor. To płytszy selektor albo layer. - Sięganie po
!importantprzy pierwszym konflikcie. Wygrywa bieżące starcie i przygotowuje następne, bo kolejne nadpisanie nie ma już żadnego dostępnego ruchu w normalnej specyficzności. - Zapominanie, że pseudoklasy się liczą.
:hover,:focus,:nth-child()i argument:not()wszystkie dodają0-1-0. Zwykłea { color: blue; }niżej w pliku nie nadpiszea:not(.disabled) { color: red; }napisanego wcześniej, bo wersja z:not()jest cięższa. - Zakładanie, że dziedziczona wartość wygrywa, bo reguła “wydaje się” silniejsza. Reguła, która celuje bezpośrednio w element, zawsze bije dziedziczoną wartość, niezależnie od specyficzności reguły, z której pochodzi dziedziczenie. Jeśli element potomny pokazuje niewłaściwy kolor, szukaj reguły celującej w tego potomka, zanim podejrzewasz dziedziczenie.
- Wplatanie
:is()w selektor z nadzieją, że zostanie lekki.:is()przejmuje wagę swojego najcięższego argumentu, więc:is(.card, #sidebar)jest w praktyce przebranym selektorem1-0-0.
Jak sprawdzić specyficzność w DevTools
Nie trzeba liczyć kolumn ręcznie, gdy błąd jest już przed tobą. Zbadaj element w Chrome lub Firefox DevTools i otwórz panel Styles (Rules w Firefoksie). Każda pasująca reguła jest wylistowana w porządku kaskady, a nadpisane deklaracje są przekreślone. Najedź na selektor, a obie przeglądarki pokażą jego specyficzność. Stąd w kilka sekund widać, czy problemem jest różnica specyficzności, kolejność layerów, czy zwykła kolejność w pliku między dwiema regułami o tej samej wadze.
Następnym razem, gdy reguła przegra bez wyraźnego powodu, policz obie trójki, zanim dodasz klasę albo !important, żeby ją wymusić. Zwykle poprawnym rozwiązaniem jest zdjęcie wagi z selektora, który wygrywa, a nie dokładanie jej do tego, który powinien wygrać.