Umieszczanie w nawiasach elementów CSS uległo poprawie dzięki elementom CSSNestedDeclarations.

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

  • Chrome: 130.
  • Edge: 130.
  • Firefox: 132.
  • Safari: nieobsługiwane.

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 element CSSStyleRule 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ść cssTextCSSStyleRule.

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 typu CSSStyleDeclaration reprezentującym deklaracje.
  • Właściwość cssRules, czyli obiekt CSSRuleList, który zawiera wszystkie zagnieżdżone obiekty CSSRule.

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.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

  • Chrome: 130.
  • Edge: 130.
  • Firefox: 132.
  • Safari: nieobsługiwane.

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 cssTextCSSStyleRule. 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;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
}