CSS @layer: jak cascade layers kończą wojnę o specyficzność
CSS @layer: jak cascade layers kończą wojnę o specyficzność
Masz komponent karty z biblioteki design systemu: .card .card__header .card__title { font-size: 18px; } – trzy klasy w selektorze, bo tak zagnieździł go autor komponentu. Chcesz go przebić jedną klasą narzędziową, .text-lg { font-size: 20px; }, dodaną w HTML-u już po klasach karty. Nie działa. Tytuł uparcie zostaje przy 18px, mimo że .text-lg w kodzie widnieje później, więc intuicyjnie „powinien wygrać ten nowszy”. To nie błąd przeglądarki – trzy klasy w selektorze biją jedną klasę, niezależnie od kolejności w pliku. Tak działa specyficzność od 1996 roku, i tak ma działać.
Klasyczne obejście to podbicie specyficzności własnej reguły – .card .text-lg, albo, gdy to nie wystarcza, !important. Oba rozwiązania działają punktowo i psują się systemowo: za każdym razem, gdy ktoś w zespole doda regułę z jeszcze wyższą specyficznością (albo jeszcze jeden !important), cała reszta arkusza staje się nieprzewidywalna. To właśnie ten problem – nie estetyczny, tylko architektoniczny – rozwiązują CSS Cascade Layers.
Nowa oś rozstrzygania konfliktów, nie kolejny sposób na podbicie priorytetu
Kluczowa rzecz do zrozumienia: @layer nie jest jeszcze jednym narzędziem do zwiększania priorytetu reguły, tak jak !important czy dopisanie kolejnej klasy do selektora. To osobny etap algorytmu kaskady, rozstrzygany przed specyficznością, nie obok niej. Pełna kolejność, w jakiej przeglądarka rozstrzyga konflikt między dwiema regułami CSS, wygląda (upraszczając pominięciem originu) tak:
- Warstwa (
@layer) – reguła z warstwy zadeklarowanej później wygrywa z regułą z warstwy zadeklarowanej wcześniej, niezależnie od specyficzności selektorów w środku. - Specyficzność – rozstrzyga dopiero wtedy, gdy obie reguły są w tej samej warstwie (albo obie w ogóle nie są w żadnej warstwie).
- Kolejność w źródle – ostatni sędzia, gdy poprzednie dwa kryteria dają remis.
To sprawia, że jedna klasa .text-lg w późniejszej warstwie może pokonać selektor z trzema klasami we wcześniejszej warstwie – bo starcie w ogóle nigdy nie dochodzi do etapu porównywania specyficzności. Warstwa rozstrzyga sprawę wcześniej.
css
/* Kolejność warstw deklarujesz raz, z góry – nie musi pokrywać sięz kolejnością, w jakiej ich zawartość faktycznie pojawia się w plikach */@layer reset, base, components, utilities;@layer components {.card .card__header .card__title {font-size: 18px;}}@layer utilities {.text-lg {font-size: 20px;}}
Ta pusta deklaracja @layer reset, base, components, utilities; na górze pliku to nie formalność – to jawne ustalenie kolejności priorytetów, zanim jakakolwiek z tych warstw dostanie realną treść. Dzięki temu kolejność warstw jest niezależna od kolejności importów czy plików – components może fizycznie znaleźć się w kodzie przed utilities, a mimo to utilities i tak wygra, bo taką kolejność zadeklarowano na starcie.
Praktyczny przykład: reset, komponenty i narzędziówka, które nigdy nie walczą ze sobą
W praktyce ten wzorzec – reset niżej niż komponenty, komponenty niżej niż klasy narzędziowe – to dokładnie to, jak działa np. system warstw w Tailwindzie od wersji 3.1: klasa narzędziowa ma zawsze wygrywać z regułą komponentu, niezależnie od tego, ile klas ma selektor tego komponentu, bo to właśnie po to klasy narzędziowe w ogóle istnieją.
css
@layer reset, base, components, utilities;@layer reset {* {margin: 0;padding: 0;box-sizing: border-box;}}@layer base {body {font-family: system-ui, sans-serif;color: #1a1a1a;}}@layer components {.card {border: 1px solid #e0e0e0;border-radius: 12px;padding: 16px;}.card .card__header .card__title {font-size: 18px;font-weight: 600;}}@layer utilities {.text-lg {font-size: 20px !important;}.mt-4 {margin-top: 16px;}}
Zwróć uwagę, że w warstwie utilities !important przy .text-lg jest tu zbędny – warstwa już zapewnia priorytet, bez konieczności sięgania po najcięższą broń CSS. To dobra ilustracja, dlaczego cascade layers realnie redukują liczbę !important w kodzie: przestajesz go potrzebować jako obejścia specyficzności, bo masz do dyspozycji narzędzie zaprojektowane dokładnie do tego zadania.
Ten sam mechanizm rozwiązuje drugi, równie częsty problem – bibliotekę CSS z zewnątrz, której selektorów nie kontrolujesz i której priorytet chcesz mieć zawsze niższy niż Twoje własne style, bez podbijania specyficzności własnych reguł:
css
@import url("some-ui-library.css") layer(vendor);@layer vendor, base, components, utilities;
Cała biblioteka trafia do warstwy vendor, umieszczonej na samym początku kolejności – więc Twoje base, components i utilities zawsze ją przebiją, niezależnie od tego, jak agresywnie ta biblioteka buduje swoje selektory.
Pułapka, która łamie intuicję: style bez warstwy biją wszystkie warstwy
To najczęściej pomijany, a zarazem najbardziej konsekwencyjny szczegół całego mechanizmu: reguła, która nie jest w żadnej warstwie, wygrywa z regułą w dowolnej warstwie – dla zwykłych, nie-!important-owych deklaracji. Nie „przegrywa, bo jest w niewidzialnej warstwie na końcu” – wygrywa zawsze, bezwarunkowo, z każdą regułą warstwową, choćby ta ostatnia była w warstwie zadeklarowanej jako najwyższy priorytet.
css
@layer utilities {.text-lg {font-size: 20px;}}/* Ta reguła, mimo że nie jest w żadnej warstwie i wygląda jakprzypadkowy, pojedynczy override – WYGRYWA z .text-lg powyżej,niezależnie od tego, gdzie w pliku się znajduje */.title {font-size: 16px;}
To zachowanie jest celowe – dzięki niemu stopniowe wdrażanie cascade layers do istniejącego, dużego projektu jest bezpieczne: cały „stary” CSS, który jeszcze nie trafił do żadnej warstwy, domyślnie zachowuje najwyższy priorytet, więc nie zaczyna nagle przegrywać z nowo dodanymi warstwami. Ale to samo zachowanie jest źródłem najbardziej mylących bugów w zespołach, które wdrażają @layer częściowo: pojedyncza reguła zostawiona przypadkiem poza warstwą – literówka, zapomniany import, blok <style> wstrzyknięty przez trzecią bibliotekę – przebije cały starannie zaprojektowany system priorytetów, bez żadnego ostrzeżenia. Konsekwencja praktyczna: jeśli decydujesz się na cascade layers, konsekwentnie wrzucaj wszystko do jakiejś warstwy, łącznie z resetem i stylami bazowymi – nie zostawiaj żadnej reguły „na zewnątrz”, bo to ona, nie warstwa utilities, wygra każdy konflikt.
Druga pułapka: !important odwraca kolejność warstw
Jeśli sięgniesz po !important wewnątrz warstwy, kolejność priorytetów między warstwami się odwraca. Dla zwykłych deklaracji warstwa zadeklarowana później wygrywa. Dla deklaracji z !important – wygrywa warstwa zadeklarowana wcześniej.
css
@layer reset, components;@layer reset {.card { padding: 0 !important; }}@layer components {.card { padding: 16px !important; }}/* Wygrywa reset (0px), bo przy !important pierwszeństwo mawarstwa zadeklarowana wcześniej – dokładne odwrócenie zasadyobowiązującej bez !important */
To nie jest kaprys specyfikacji – to konsekwentne rozszerzenie zasady, która już istniała dla originów (arkusze !important użytkownika od zawsze przebijały !important autora strony, odwrotnie niż przy zwykłych deklaracjach). Praktyczny wniosek: !important wewnątrz systemu warstw potrafi zaskoczyć nawet kogoś, kto dobrze rozumie zwykłą kolejność @layer – kolejny dobry powód, żeby traktować !important jako coś, po co sięgasz świadomie i rzadko, a nie domyślny sposób na wygranie starcia.
Pułapki i dobre praktyki
- Zero reguł poza warstwami, jeśli system ma być przewidywalny. Jak pokazano wyżej, nawet jedna reguła bez
@layerbije cały system warstw dla zwykłych deklaracji – konsekwencja jest ważniejsza niż wygoda dopisania „tylko jednej małej reguły” w locie. - Specyficzność wciąż obowiązuje wewnątrz pojedynczej warstwy.
@layereliminuje wojny specyficzności między warstwami, ale w środku jednej warstwy dwie reguły nadal rozstrzygają konflikt tak jak zawsze – sensowne nazewnictwo klas i unikanie głębokiego zagnieżdżania selektorów wciąż się opłaca. - Deklaruj kolejność warstw jawnie, na samej górze, pustym
@layer nazwa1, nazwa2, ...;. To zdejmuje z Ciebie zależność od kolejności importów plików – kolejność priorytetów jest ustalona raz, czytelnie, w jednym miejscu, niezależnie od tego, w jakiej kolejności poszczególne pliki CSS faktycznie się później ładują. - Wsparcie przeglądarek nie jest już powodem do ostrożności. Cascade Layers osiągnęły status Baseline (szeroko dostępne) już w 2022 roku – Chrome 99, Firefox 97, Safari 15.4 – więc można wdrażać je bez
@supportsw każdym nowym projekcie.
Podsumowanie
@layer nie konkuruje ze specyficznością – działa na zupełnie innym etapie algorytmu kaskady, wcześniej, i dzięki temu pozwala jednej klasie narzędziowej świadomie i przewidywalnie pokonać selektor z trzema zagnieżdżonymi klasami, bez podbijania priorytetu i bez !important. To realne narzędzie architektoniczne do rozdzielenia resetu, stylów bazowych, komponentów i narzędziówki na warstwy o jawnie zadeklarowanej kolejności ważności – ale dwie jego zasady łamią intuicję na tyle mocno, że warto je zapamiętać osobno: styl poza jakąkolwiek warstwą bije każdą warstwę, a !important w środku warstw odwraca kolejność ich priorytetu. Zignorowanie któregokolwiek z tych dwóch faktów nie skończy się błędem w konsoli – skończy się CSS-em, który działa inaczej, niż wynikałoby to z pozornie logicznej kolejności warstw w pliku.