Opublikowano: 8 października 2024 r.
Aby naprawić niektóre dziwne błędy związane z zagnieżdżeniem CSS, grupa robocza CSS postanowiła dodać interfejs CSSNestedDeclarations
do specyfikacji zagnieżdżania CSS. Dzięki temu dodatkowi deklaracje, które występują po regułach stylów, nie przesuwają się już w górę. Wprowadziliśmy też kilka innych ulepszeń.
Te zmiany są dostępne w Chrome od wersji 130 i są gotowe do testowania w Firefox Nightly 132 i Safari Technology Preview 204.
Obsługa przeglądarek
Problem z zagnieżdżaniem CSS bez CSSNestedDeclarations
Jednym z problemów z zagnieżdżaniem CSS jest to, że początkowo ten fragment kodu nie działa tak, jak można się spodziewać:
.foo {
width: fit-content;
@media screen {
background-color: red;
}
background-color: green;
}
Patrząc na kod, można założyć, że element <div class=foo>
ma wartość green
background-color
, ponieważ deklaracja background-color: green;
jest ostatnia. W Chrome w wersji wcześniejszej niż 130 nie było to jednak możliwe. W tych wersjach, które nie obsługują CSSNestedDeclarations
, background-color
elementu ma wartość red
.
Po przeanalizowaniu rzeczywistej reguły Chrome przed wersją 130 użyto w następujący sposób:
.foo {
width: fit-content;
background-color: green;
@media screen {
& {
background-color: red;
}
}
}
Kod CSS po przeanalizowaniu uległ 2 zmianom:
background-color: green;
została przesunięta w górę, aby dołączyć do pozostałych deklaracji.- Zagnieżdżony obiekt
CSSMediaRule
został zmieniony, aby pakować jego deklaracje w dodatkowy elementCSSStyleRule
za pomocą selektora&
.
Inną typową zmianą, którą tu widzisz, jest odrzucenie przez parsujący właściwości, których nie obsługuje.
Możesz sprawdzić „CSS po przetworzeniu”, odczytując wartość cssText
z CSSStyleRule
.
Wypróbuj to samodzielnie w tym interaktywnym narzędziu:
Dlaczego ten kod CSS jest przepisywany?
Aby zrozumieć, dlaczego tak się stało, musisz dowiedzieć się, jak ten element CSSStyleRule
jest prezentowany w modelu obiektowym CSS (CSSOM).
W Chrome w wersji wcześniejszej niż 130 udostępniony wcześniej fragment kodu CSS serializuje się w ten sposób:
↳ CSSStyleRule
.type = STYLE_RULE
.selectorText = ".foo"
.resolvedSelectorText = ".foo"
.specificity = "(0,1,0)"
.style (CSSStyleDeclaration, 2) =
- width: fit-content
- background-color: green
.cssRules (CSSRuleList, 1) =
↳ CSSMediaRule
.type = MEDIA_RULE
.cssRules (CSSRuleList, 1) =
↳ CSSStyleRule
.type = STYLE_RULE
.selectorText = "&"
.resolvedSelectorText = ":is(.foo)"
.specificity = "(0,1,0)"
.style (CSSStyleDeclaration, 1) =
- background-color: red
Spośród wszystkich właściwości elementu CSSStyleRule
w tym przypadku istotne są 2 z nich:
- Właściwość
style
, która jest wystąpieniem typuCSSStyleDeclaration
reprezentującym deklaracje. - Właściwość
cssRules
, czyli obiektCSSRuleList
, który zawiera wszystkie zagnieżdżone obiektyCSSRule
.
Wszystkie deklaracje z fragmentu kodu CSS trafiają do właściwości style
obiektu CSStyleRule
, co powoduje utratę informacji. Przyglądając się właściwości style
, nie widać, że element background-color: green
został zadeklarowany po zagnieżdżonym elemencie CSSMediaRule
.
↳ CSSStyleRule
.type = STYLE_RULE
.selectorText = ".foo"
.style (CSSStyleDeclaration, 2) =
- width: fit-content
- background-color: green
.cssRules (CSSRuleList, 1) =
↳ …
Jest to problematyczne, ponieważ aby silnik CSS działał prawidłowo, musi umieć odróżnić właściwości, które pojawiają się na początku treści reguły stylu, od tych, które pojawiają się wśród innych reguł.
Jeśli chodzi o deklaracje w elementach CSSMediaRule
, które nagle zostały zawinięte w element CSSStyleRule
: element CSSMediaRule
nie został zaprojektowany tak, aby zawierał deklaracje.
Ponieważ CSSMediaRule
może zawierać zagnieżdżone reguły, do których można uzyskać dostęp za pomocą właściwości cssRules
, deklaracje są automatycznie otaczane elementem CSSStyleRule
.
↳ CSSMediaRule
.type = MEDIA_RULE
.cssRules (CSSRuleList, 1) =
↳ CSSStyleRule
.type = STYLE_RULE
.selectorText = "&"
.resolvedSelectorText = ":is(.foo)"
.specificity = "(0,1,0)"
.style (CSSStyleDeclaration, 1) =
- background-color: red
Jak rozwiązać ten problem?
Grupa robocza CSS przeanalizowała kilka opcji rozwiązania tego problemu.
Jednym z zaproponowanych rozwiązań było owinięcie wszystkich deklaracji bez nazwy w zagnieżdżonym elemencie CSSStyleRule
za pomocą selektora zagnieżdżonego (&
). Ta propozycja została odrzucona z różnych powodów, w tym z powodu tych niepożądanych efektów ubocznych zastąpienia &
elementem :is(…)
:
- Wpływa to na specyficzność. Dzieje się tak, ponieważ funkcja
:is()
przejmuje szczegółowość najbardziej szczegółowego argumentu. - Nie działa on dobrze w przypadku pseudoelementów w pierwotnym selektorze zewnętrznym. Dzieje się tak, ponieważ funkcja
:is()
nie akceptuje pseudoelementów w swoim argumencie listy selektora.
Weź pod uwagę ten przykład:
#foo, .foo, .foo::before {
width: fit-content;
background-color: red;
@media screen {
background-color: green;
}
}
Po przeanalizowaniu tego fragmentu kodu w Chrome w wersji wcześniejszej niż 130 wygląda on tak:
#foo,
.foo,
.foo::before {
width: fit-content;
background-color: red;
@media screen {
& {
background-color: green;
}
}
}
Jest to problem, ponieważ element CSSRule
jest zagnieżdżony w selektorze &
:
- Sprowadza się do
:is(#foo, .foo)
, wyrzucając.foo::before
z listy selektorów. - Ma precyzję równą
(1,0,0)
, co utrudnia jego późniejsze zastąpienie.
Możesz to sprawdzić, sprawdzając, do czego sprowadza się serializacja reguły:
↳ CSSStyleRule
.type = STYLE_RULE
.selectorText = "#foo, .foo, .foo::before"
.resolvedSelectorText = "#foo, .foo, .foo::before"
.specificity = (1,0,0),(0,1,0),(0,1,1)
.style (CSSStyleDeclaration, 2) =
- width: fit-content
- background-color: red
.cssRules (CSSRuleList, 1) =
↳ CSSMediaRule
.type = MEDIA_RULE
.cssRules (CSSRuleList, 1) =
↳ CSSStyleRule
.type = STYLE_RULE
.selectorText = "&"
.resolvedSelectorText = ":is(#foo, .foo, .foo::before)"
.specificity = (1,0,0)
.style (CSSStyleDeclaration, 1) =
- background-color: green
Wizualnie oznacza to też, że background-color
z .foo::before
to red
zamiast green
.
Innym podejściem, które wzięliśmy pod uwagę, było zawijanie wszystkich zagnieżdżonych deklaracji w regułę @nest
. Zostało to odrzucone ze względu na pogorszenie się komfortu pracy programistów.
Przedstawiamy interfejs CSSNestedDeclarations
Rozwiązaniem, na którym zdecydowała się grupa robocza CSS, jest wprowadzenie reguły deklaracji zagnieżdżonych.
Ta reguła dotycząca zagnieżdżonych deklaracji jest implementowana w Chrome od wersji 130.
Obsługa przeglądarek
Wprowadzenie reguły dotyczącej zagnieżdżonych deklaracji powoduje, że parsujący kod CSS automatycznie umieszcza kolejne bezpośrednio zagnieżdżone deklaracje w instancji CSSNestedDeclarations
. Po zserializowaniu to wystąpienie CSSNestedDeclarations
trafia do właściwości cssRules
obiektu CSSStyleRule
.
Ponownie weźmy pod uwagę konto CSSStyleRule
:
.foo {
width: fit-content;
@media screen {
background-color: red;
}
background-color: green;
}
W Chrome 130 lub nowszej serializacja wygląda tak:
↳ CSSStyleRule
.type = STYLE_RULE
.selectorText = ".foo"
.resolvedSelectorText = ".foo"
.specificity = (0,1,0)
.style (CSSStyleDeclaration, 1) =
- width: fit-content
.cssRules (CSSRuleList, 2) =
↳ CSSMediaRule
.type = MEDIA_RULE
.cssRules (CSSRuleList, 1) =
↳ CSSNestedDeclarations
.style (CSSStyleDeclaration, 1) =
- background-color: red
↳ CSSNestedDeclarations
.style (CSSStyleDeclaration, 1) =
- background-color: green
Ponieważ reguła CSSNestedDeclarations
kończy się w sekwencji CSSRuleList
, parsujący może zachować pozycję deklaracji background-color: green
: po deklaracji background-color: red
(która jest częścią CSSMediaRule
).
Ponadto instancja CSSNestedDeclarations
nie powoduje żadnych nieprzyjemnych skutków ubocznych, które występowały w innych, odrzuconych już potencjalnych rozwiązaniach: reguła zagnieżdżonych deklaracji pasuje do dokładnie tych samych elementów i pseudoelementów co reguła stylu nadrzędnego, zachowując tę samą specyficzność.
Potwierdzeniem tego jest odczytanie cssText
z CSSStyleRule
. Dzięki regułom dotyczącym zagnieżdżonych deklaracji jest ona taka sama jak usługa porównywania cen wejściowych:
.foo {
width: fit-content;
@media screen {
background-color: red;
}
background-color: green;
}
Co to oznacza dla Ciebie
Oznacza to, że od wersji 130 Chrome zagnieżdżanie CSS znacznie się poprawiło. Oznacza to jednak również, że w przypadku przeplatania nagich deklaracji z regułami zagnieżdżonymi może być konieczne przejrzenie części kodu.
Rozważ przykład, który korzysta z wspaniałego elementu @starting-style
.
/* This does not work in Chrome 130 */
#mypopover:popover-open {
@starting-style {
opacity: 0;
scale: 0.5;
}
opacity: 1;
scale: 1;
}
Przed wersją 130 te deklaracje były przenoszone. W rezultacie deklaracje opacity: 1;
i scale: 1;
zostaną umieszczone w deklaracji CSSStyleRule.style
, a następnie w deklaracji CSSStyleRule.cssRules
zostanie umieszczona deklaracja CSSStartingStyleRule
(odpowiadająca regule @starting-style
).
Począwszy od Chrome 130 deklaracje nie są już podnoszone, więc w obiekcie CSSStyleRule.cssRules
masz 2 zagnieżdżone obiekty CSSRule
. W kolejności: 1 CSSStartingStyleRule
(reprezentująca regułę @starting-style
) i 1 CSSNestedDeclarations
, która zawiera deklaracje opacity: 1; scale: 1;
.
Z powodu tej zmiany deklaracje @starting-style
są zastępowane deklaracją zawartą w instancji CSSNestedDeclarations
, co powoduje usunięcie animacji wejścia.
Aby naprawić kod, upewnij się, że blok @starting-style
znajduje się po zwykłych deklaracjach. W ten sposób:
/* This works in Chrome 130 */
#mypopover:popover-open {
opacity: 1;
scale: 1;
@starting-style {
opacity: 0;
scale: 0.5;
}
}
Jeśli umieszczasz zagnieżdżone deklaracje nad zagnieżdżonymi regułami podczas zagnieżdżania kodu CSS, Twój kod działa większości dobrze we wszystkich wersjach wszystkich przeglądarek, które obsługują zagnieżdżanie CSS.
Jeśli chcesz wykryć dostępność funkcji CSSNestedDeclarations
, możesz użyć tego fragmentu kodu JavaScript:
if (!("CSSNestedDeclarations" in self && "style" in CSSNestedDeclarations.prototype)) {
// CSSNestedDeclarations is not available
}