Jak kaskada wybiera zwycięzcę: znaczenie, specyfika i kolejność źródeł
Zobacz, jak przeglądarki rozstrzygają między sprzecznymi deklaracjami CSS, jak odczytywać specyfikę jako porównanie czterech elementów oraz dlaczego reguły typu hover i !important tak często cię zaskakują.
Gdy kilka reguł CSS dotyczy tego samego elementu i ustawia tę samą właściwość, przeglądarka nie może zastosować ich wszystkich. Potrzebuje więc deterministycznego sposobu na wybór dokładnie jednej deklaracji, a ten proces tłumaczy całą klasę błędów typu „dlaczego mój styl jest ignorowany?”. Po przeczytaniu tego przewodnika będziesz w stanie obliczyć specyficzność selektora, przewidzieć, która deklaracja zwycięży, oraz naprawiać trudne przypadki, takie jak reguła :hover, która nigdy się nie aktywuje, bez konieczności używania !important.
Miejsce tego w procesie renderowania
HTML
↓
DOM
↓
CSS
↓
CSSOM
↓
Render Tree
↓
Layout
↓
Paint
↓
Pixels
Wewnątrz procesu CSS kryją się trzy powiązane pytania. Po pierwsze, gdy konkuruje kilka deklaracji, która z nich wygrywa? Po drugie, po wybraniu zwycięzcy, jaka jest faktyczna wartość, do której dochodzi? Po trzecie, co się dzieje, gdy element w ogóle nie ma wartości dla danej właściwości? Odpowiedziami są kaskada (która opiera się na specyficzności), przetwarzanie wartości oraz dziedziczenie. Często są one omawiane jako niepowiązane tematy, ale w przeglądarce stanowią kolejne kroki tego samego procesu. Ten przewodnik koncentruje się na pierwszym pytaniu. Jeśli chcesz uzyskać więcej informacji na temat tego, co dzieje się po zastosowaniu stylów, sprawdź jak przeglądarka rysuje elementy i jakie ma znaczenie React.
Zasady, deklaracje i konkurencyjne wartości
Najpierw krótka informacja o słownictwie. Reguła CSS składa się z selektora, po którym następuje blok deklaracji:
.button {
background-color: blue;
}
W tej regule .button jest selektorem, background-color: blue; to deklaracja, background-color to właściwość, a blue to wartość. Wartość zapisana dokładnie tak, jak ją napisałeś, nazywana jest deklarowaną wartością.
Prawdziwy plik stylów często zawiera kilka deklaracji dla tej samej właściwości w tym samym elemencie. Jedna reguła może odnosić się do wszystkich przycisków:
button {
background-color: red;
}
podczas gdy inne, być może w innym pliku, odnoszą się do przycisków ogólnie oraz do konkretnego przycisku poprzez jego id:
button {
background-color: blue;
}
#submit {
background-color: green;
}
Jeśli pojedynczy element <button id="submit"> pasuje do wszystkich trzech reguł, jaki kolor tła powinien mieć? Rozstrzygnięcie tego jest zadaniem kaskady. Rozwiązuje ona konflikty, biorąc pod uwagę znaczenie każdej deklaracji, specyficzność jej selektora oraz kolejność pojawiania się deklaracji. Po uwzględnieniu znaczenia, to zazwyczaj specyficzność decyduje o wyniku.
Pełna kaskada określona w specyfikacji uwzględnia również pochodzenie arkusza stylów (domyślne ustawienia przeglądarki, style użytkownika, style autora) oraz, w nowoczesnym CSS, warstwy kaskady zadeklarowane za pomocą @layer. W przypadku codziennych arkuszy stylów autora bez warstw, to znaczenie, specyficzność i kolejność źródeł stanowią trzy kluczowe czynniki, o których należy myśleć.
Czym jest specyficzność
Specyficzność to miara określana przez przeglądarkę, jak dokładnie selektor celuje w dany element. Nie wszystkie selektory mają taką samą wagę. Porównajmy selektor typu:
p {
color: red;
}
selektor klasy:
.text {
color: blue;
}
i selektor id:
#title {
color: green;
}
Jeśli wszystkie trzy pasują do tego samego elementu, przeglądarka sortuje je według ustalonej hierarchii rodzajów selektorów, od najsilniejszych do najsłabszych:
Inline styles
↓
IDs
↓
Classes / pseudo-classes / attributes
↓
Elements / pseudo-elements
Gdy konkurencyjne deklaracje mają równą wagę, wygrywa bardziej specyficzny selektor. W tym przypadku zwyciężyłby reguła id, a tekst byłby zielony.
Ważna uwaga dotycząca górnej wiersza: style inline ustawione za pomocą atrybutu style nie są selektorami, a obecna specyfikacja traktuje je jako odrębny krok, który ma pierwszeństwo przed wszelkimi deklaracjami autora opartymi na selektorach. Traktowanie ich jako najwyższej „kolumny” specyficzności, jak to powszechnie praktykuje się, daje w praktyce ten sam wynik, dlatego reszta tego przewodnika zachowuje to wygodne podejście.
Odczytywanie specyficzności jako czterech kolumn
Najczęstszym błędem jest przekonanie, że specyficzność to pojedyncza wartość, którą można sumować. Lepiej jest ją rozumieć jako zbiór czterech liczb, po jednej dla każdej kategorii:
Inline | IDs | Classes | Elements
Dla danego selektora liczy się, ile elementów należy do każdej kategorii. Najprostszym przypadkiem jest selektor klasy:
.button {
background: blue;
}
Nie zawiera on żadnego stylu inline, żadnego id, jednej klasy ani żadnych elementów:
Inline styles → 0
IDs → 0
Classes → 1
Elements → 0
co można zapisać w zwięzły sposób jako:
0, 0, 1, 0
Zobaczmy teraz bardziej złożony selektor:
nav#main .button div {
background: green;
}
Zawiera on jeden id (#main), jedną klasę (.button) oraz dwa selektory typu (nav i div), więc jego specyficzność wynosi:
0, 1, 1, 2
Aby porównać dwa selektory, przeglądarka czyta te pary od lewej do prawej, zaczynając od najważniejszej kolumny. Pierwsza kolumna, w której występuje różnica, decyduje o wyniku, a kolumny po niej już nie mają znaczenia. Dlatego jeden id ma większą wagę niż dowolna liczba klas, a jedna klasa ma większą wagę niż dowolna liczba selektorów typu: nie ma przenoszenia wartości z jednej kolumny do drugiej, więc dziesięć klas nigdy nie „dodaje się” do jednego id. Nie musisz zapamiętywać arytmetyki; wystarczy pamiętać kolejność porównań.
Rozwiązywanie rzeczywistego konfliktu
Rozważmy przycisk, który ma zarówno klasę, jak i id. Należy zauważyć, że to zwykła metka HTML, więc używa class zamiast className z JSX:
<button class="button" id="submit">
Don't Click
</button>
Załóżmy teraz, że plik stylów zawiera regułę klasową:
.button {
background: blue;
}
oraz kilka innych, w tym selektor typu, długi selektor potomków oraz regułę id-plus-class z stanem hover:
button {
background: purple;
}
nav#main .button div {
background: green;
}
#submit.button:hover {
background: yellow;
}
Wszystkie one deklarują background, więc konkurują ze sobą. Przeglądarka nie wybiera po prostu tej, która występuje ostatnia; najpierw porównuje specyficzność.
Jeden szczegół jest łatwy do przeoczenia w tym bloku: nav#main .button div faktycznie odnosi się do div umieszczonego wewnątrz elementu o klasie button, a nie do samego przycisku. Ponieważ podmiot selektora to jego najbardziej prawa część, ta zasada nigdy nie pasuje do naszego <button>, bez względu na jego specyficzność. Sprawdzenie, czy zasada w ogóle pasuje, jest zawsze pierwszym krokiem przy debugowaniu.
Dla zasad, które rzeczywiście pasują, porównaj selektor klasowy:
.button
z prostym selektorem typu:
button
Pierwszy zawiera jedną klasę, drugi tylko jeden element. Zatem:
.button
ma wyższą prioritetowość:
button
a przycisk jest niebieski, a nie fioletowy, mimo że zasada fioletowa pojawia się później.
Dodaj id do układu:
#submit.button
Id umieszcza ten selektor w wyższej kolumnie niż wszystko, co składa się wyłącznie z klas i typów. Porównując od lewej do prawej, kolumna id natychmiast rozstrzyga sprawę, dlatego jedno id może przeważyć nad selektorem składającym się z wielu klas.
Dlaczego poprawna reguła :hover może nic nie zrobić
Taki przypadek powoduje wiele zamieszania podczas debugowania. Zacznij od reguły bazowej dla przycisku:
#submit.button {
background: red;
}
i reguły hover dla tego samego elementu:
#submit.button:hover {
background: yellow;
}
Reguła hover zawiera jedno id, jedną klasę i jedną pseudo-klasę, co daje jej większą specyficzność niż reguła bazowa, więc po nadaniu efektu hover przycisk staje się żółty, zgodnie z oczekiwaniami.
A teraz wyobraź sobie, że inna część kodu stylizuje ten sam przycisk za pomocą znacznie dłuższego selektora:
nav#main div#container #submit.button {
background: red;
}
podczas gdy reguła hover pozostaje niezmieniona:
#submit.button:hover {
background: yellow;
}
Reguła hover nadal zawiera pseudo-klasę:
:hover
Pseudo-klasy są uwzględniane w kolumnie z klasami. Jednak dają one tylko jeden punkt na poziomie klasy. Długi selektor zawiera trzy identyfikatory, podczas gdy reguła hover ma tylko jeden, więc wygrywa on w kolumnie z identyfikatorami jeszcze przed porównaniem klas. W rezultacie powstaje reguła :hover, która jest składniowo doskonała, pasuje do elementu, ale nadal nic nie zmienia na ekranie.
Lekcją jest to, że gdy stany interakcji wydają się nieprawidłowe, pseudo-klasa rzadko jest przyczyną. Prawdziwym problemem jest zazwyczaj to, że jakaś inna deklaracja ma wyższą specyficzność. Narzędzia deweloperskie przeglądarki ułatwiają to dostrzeżenie: panel Styles wyświetla wszystkie pasujące reguły i przekreśla te, które przegrały, co pokazuje dokładnie, który selektor jest silniejszy od twojego.
Równości rozstrzyga kolejność źródłowa
Czasami dwa selektory mają identyczną specyficzność. Weźmy dwie reguły z tym samym selektorem klasy:
.button {
background: red;
}
.button {
background: blue;
}
Są one równie specyficzne i równie ważne, więc żaden z pierwszych dwóch poziomów nie może podjąć decyzji. Wtedy przeglądarka odwołuje się do porządku źródła: zwycięża deklaracja, która pojawia się później. W tym porządku:
.button {
background: red;
}
.button {
background: blue;
}
przycisk kończy się na niebiesko.
Cały proces decyzyjny można przedstawić jako sekwencję reguł rozstrzygających w przypadku remisu:
Importance
↓
Specificity
↓
Source Order
Każdy poziom jest sprawdzany tylko wtedy, gdy poprzedni nie był w stanie wyłonić zwycięzcy. Najpierw bierze się pod uwagę wagę; jeśli jest remis, decyduje specyficzność; jeśli i to nie rozstrzyga sprawy, zwycięża ostatnia deklaracja.
Koszt ucieczki do !important
Prawie każdy programista korzystał z tej opcji awaryjnej przynajmniej raz:
color: red !important;
Dodanie !important zwiększa priorytet deklaracji. Ponieważ priorytet jest sprawdzany przed specyfiką, deklaracja oznaczona w ten sposób może przezwyciężyć inną deklarację o znacznie wyższej specyfice. Na przykład:
.button {
background: purple !important;
}
przezwycięży zwykłą deklarację w przypadku długiego selektora zawierającego wiele identyfikatorów. Gdy dwie deklaracje !important konkurują ze sobą, przeglądarka ponownie porównuje ich specyfikę, a następnie kolejność.
To właśnie ta moc sprawia, że jest to ryzykowne. Charakterystyczna spirala debugowania wygląda w ten sposób:
"My style isn't working."
↓
"Let's increase the specificity."
↓
"Still not working."
↓
"Let's add !important."
↓
"It works!"
Styl w końcu się pojawia, ale podstawowy konflikt nie zniknął; został przeniesiony na następną osobę, która musi przejąć kontrolę nad tą właściwością i teraz musi walczyć ze swoim własnym !important. W miarę gromadzenia się takich przypadków plik stylów staje się coraz trudniejszy do zrozumienia. Traktuj !important jako ostateczność, a nagłą potrzebę jego użycia postrzegaj jako sygnał, że CSS prawdopodobnie wymaga refaktoryzacji.
Zanim coś napiszesz:
!important
zadaj bardziej użyteczne pytanie: dlaczego moja deklaracja nie przegrywa? Następnie sprawdź poziomy w kolejności:
- Czy istnieje bardziej ważna deklaracja konkurencyjna?
- Czy istnieje bardziej specyficzny selektor konkurencyjny?
- Czy deklaracja konkurencyjna pojawia się później w kodzie źródłowym?
Istnieją rzeczywiście uzasadnione zastosowania, takie jak klasy pomocnicze przeznaczone do ciągłego stosowania określonych stylów lub przejęcia stylów wstawionych przez komponent third-party, który nie można zmienić, ale powinny one być stosowane celowo, a nie automatycznie.
Pisanie selektorów, które naturalnie przeważają
W tym kontekście istotny jest również aspekt utrzymywalności. Gdy dany styl się nie stosuje, kusi nas pomysł wydłużania selektorów, aż w końcu się to uda:
body div section nav ul li a.button {
color: red;
}
To działa, ale każda dodatkowa część selektorów podnosi próg trudności przy przyszłym przejęciu stylów, łączy styl z konkretną strukturą DOM i utrudnia czytanie pliku stylów. Zamiast zastanawiać się, jak za wszelką cenę sprawić, by selektor przeważył, należy zastanowić się, jak zorganizować CSS tak, aby zamierzona deklaracja sama przeważyła. Utrzymywanie większości selektorów przy jednej klasie, unikanie używania identyfikatorów do stylizacji oraz grupowanie bardziej specyficznych wartości przejętych w pobliżu reguł, które modyfikują, bardzo pomaga.
To ma największe znaczenie w dużych bazach kodu, gdzie wiele osób pisze CSS. Specyficzność ma na celu zapewnienie przewidywalnych wyników, a nie stworzenie wyścigu zbrojeń pomiędzy selektorami.
Wykorzystywanie kolejności plików źródłowych z arkuszami stylów od third party
Kolejność plików źródłowych staje się praktycznym narzędziem, gdy łączysz własne style z plikiem resetującym lub arkuszem stylów od third party. Te pliki zazwyczaj definiują style dla powszechnie używanych elementów, a ty chcesz, aby twoje reguły je przejęły. Załaduj swojego arkusza stylów po ich plikach, aby było to proste:
<link rel="stylesheet" href="reset.css">
<link rel="stylesheet" href="style.css">
Gdy twoje selektory mają taką samą specyficzność jak te z biblioteki, wygrywa późniejszy plik, więc umieszczenie style.css po reset.css pozwala twoim deklaracjom wejść w życie bez nadmiernego mnożenia selektorów.
Zależność od kolejności ma również swoje koszty. Jeśli ktoś później ponownie ułoży tagi <link> lub elementy importowane w punkcie wejścia bundlera, style mogą się bez żadnych objawów zmienić. Tam, gdzie to możliwe, wybieraj zasady o takiej specyficzności, aby jasno określić zwycięzcę, i używaj kolejności źródeł jako ostatecznego rozstrzygacza, do czego została zaprojektowana. Warstwy kaskadowe, tam gdzie obsługiwane są przez Twoje docelowe przeglądarki, to bardziej wyraźny sposób na określenie zasady „biblioteka najpierw, nasze style później”.
Po wyłonieniu zwycięzcy: wartość kaskadowa
W tym momencie pierwsze pytanie zostaje odpowiedziane. Przeglądarka zebrала wszystkie deklaracje danej właściwości, oceniła ich znaczenie, następnie specyficzność, a potem kolejność źródeł, i wybrała jedną. Ta zwycięska wartość nazywana jest wartością kaskadową.
Jednak praca jeszcze nie jest całkowicie zakończona. Załóżmy, że zwycięską deklaracją jest:
width: 66%;
Taki procent nie może zostać bezpośrednio narysowany; przeglądarka musi go nadal przetworzyć, porównując go z blokiem, w którym się znajduje, aby uzyskać rzeczywistą długość. Ten proces przetwarzania wartości, w połączeniu z dziedziczeniem właściwości, które w ogóle nie mają deklaracji, stanowi kolejny etap przekształcania CSS w piksele.
Główne wnioski
- Kaskada rozwiązuje konflikty w ustalonej kolejności: najpierw ważność, potem specyficzność, a następnie kolejność źródeł.
- Specyficzność jest określana poprzez porównanie czterech kolumn (inline, identyfikatory, klasy/pseudo-klasy/atrybuty, elementy/pseudo-elementy), czytane od lewej do prawej; wartości z niższych kolumn nigdy nie przechodzą do tych wyższych.
- Niechcący sprawdź, czy reguła rzeczywiście pasuje do elementu, zanim porównasz jej specyficzność; najbardziej prawa część selektora określa element, do którego odnosi się ta reguła.
:hover lub innej pseudo-klasy dodaje jedynie wagę na poziomie klasy i może zostać przezwyciężona przez bardziej specyficzną zasadę bazową.!important przeważa poprzez zmianę priorytetu, a nie poprzez rozwiązanie konfliktu; należy go używać celowo i oszczędnie.Pozycje pokrewne
- Skąd pochodzą wartości CSS, gdy ustawiasz None: jak działa dziedziczenie — Dowiedz się, jak przeglądarki decydują o dziedziczeniu właściwości, dlaczego elementy potomne otrzymują obliczoną wartość rodzica oraz jak funkcje inherit i initial dają Ci wyraźną kontrolę.
- Od 66% do 185px: Jak przeglądarki rozwiązują wartości CSS przed ułożeniem — Prześledź wartość CSS na wszystkich etapach: deklarowanej, kaskadowanej, określonej, obliczonej, wykorzystanej i faktycznej, oraz dowiedz się, dlaczego jednostki względne i trik konwersji rem zachowują się w taki sposób.