CSS-Verschachtelungen werden durch CSSNestedDeclarations verbessert

Veröffentlicht: 8. Oktober 2024

Um einige merkwürdige Besonderheiten beim CSS-Verschachteln zu beheben, hat die CSS-Arbeitsgruppe beschlossen, die CSSNestedDeclarations-Schnittstelle in die CSS-Verschachtelungsspezifikation aufzunehmen. Durch diese Ergänzung werden Deklarationen, die nach Stilregeln kommen, unter anderem nicht mehr nach oben verschoben.

Diese Änderungen sind in Chrome ab Version 130 verfügbar und können in Firefox Nightly 132 und Safari Technology Preview 204 getestet werden.

Unterstützte Browser

  • Chrome: 130.
  • Edge: 130.
  • Firefox: 132
  • Safari: Nicht unterstützt.

Das Problem mit der CSS-Verschachtelung ohne CSSNestedDeclarations

Einer der Probleme bei CSS-Verschachtelungen besteht darin, dass das folgende Snippet ursprünglich nicht wie erwartet funktioniert:

.foo {
    width: fit-content;

    @media screen {
        background-color: red;
    }
    
    background-color: green;
}

Wenn Sie sich den Code ansehen, würden Sie davon ausgehen, dass das <div class=foo>-Element ein green background-color hat, da die background-color: green;-Deklaration als letztes kommt. Das ist in Chrome vor Version 130 nicht der Fall. In den Versionen, die CSSNestedDeclarations nicht unterstützen, ist der background-color des Elements red.

Nach dem Parsen der eigentlichen Regel sieht Chrome vor Version 130 so aus:

.foo {
    width: fit-content;
    background-color: green;

    @media screen {
        & {
            background-color: red;
        }
    }
}

Das CSS wurde nach dem Parsen zweimal geändert:

  • Die background-color: green; wurde nach oben verschoben, um sich den anderen beiden Deklarationen anzuschließen.
  • Der verschachtelte CSSMediaRule wurde umgeschrieben, um seine Deklarationen mithilfe des &-Sellektors in einen zusätzlichen CSSStyleRule einzubetten.

Eine weitere typische Änderung ist, dass der Parser nicht unterstützte Eigenschaften verwirft.

Sie können sich das „CSS nach dem Parsen“ selbst ansehen, indem Sie die cssText aus der CSSStyleRule zurücklesen.

Auf diesem interaktiven Spielplatz können Sie es selbst ausprobieren:

Warum wurde dieses CSS neu geschrieben?

Um zu verstehen, warum diese interne Neufassung stattgefunden hat, müssen Sie wissen, wie diese CSSStyleRule im CSS Object Model (CSSOM) dargestellt wird.

In Chrome vor Version 130 wird das zuvor freigegebene CSS-Snippet so angezeigt:

↳ 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

Von allen Properties, die ein CSSStyleRule hat, sind in diesem Fall die folgenden zwei relevant:

  • Das Attribut style, eine CSSStyleDeclaration-Instanz, die die Deklarationen darstellt.
  • Das cssRules-Attribut ist ein CSSRuleList, das alle verschachtelten CSSRule-Objekte enthält.

Da alle Deklarationen aus dem CSS-Snippet in der style-Eigenschaft des CSStyleRule landen, gehen Informationen verloren. Bei der style-Property ist nicht klar, dass background-color: green nach dem verschachtelten CSSMediaRule deklariert wurde.

↳ CSSStyleRule
  .type = STYLE_RULE
  .selectorText = ".foo"
  .style (CSSStyleDeclaration, 2) =
    - width: fit-content
    - background-color: green
  .cssRules (CSSRuleList, 1) =
    ↳ …

Das ist problematisch, da eine CSS-Engine ordnungsgemäß funktionieren muss, um Eigenschaften, die am Anfang des Inhalts einer Stilregel stehen, von denen unterscheiden zu können, die zwischen anderen Regeln eingestreut sind.

Warum die Deklarationen innerhalb von CSSMediaRule plötzlich in CSSStyleRule eingerückt werden, liegt daran, dass CSSMediaRule nicht für Deklarationen vorgesehen ist.

Da CSSMediaRule verschachtelte Regeln enthalten kann, auf die über die Property cssRules zugegriffen werden kann, werden die Deklarationen automatisch in eine CSSStyleRule verpackt.

↳ 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

Wie kann ich das Problem beheben?

Die CSS-Arbeitsgruppe hat verschiedene Möglichkeiten zur Lösung dieses Problems untersucht.

Eine der vorgeschlagenen Lösungen bestand darin, alle einfachen Deklarationen in eine verschachtelte CSSStyleRule mit dem Verschachtelungsauswahlschalter (&) einzubetten. Diese Idee wurde aus verschiedenen Gründen verworfen, darunter die folgenden unerwünschten Nebenwirkungen der Desugaring von & zu :is(…):

  • Sie hat Auswirkungen auf die Spezifität. Das liegt daran, dass :is() die Spezifizität seines spezifischsten Arguments übernimmt.
  • Sie funktioniert nicht gut mit Pseudoelementen in der ursprünglichen äußeren Auswahl. Das liegt daran, dass :is() keine Pseudoelemente in seinem Argument für die Selektorliste akzeptiert.

Siehe hierzu das folgende Beispiel:

#foo, .foo, .foo::before {
  width: fit-content;
  background-color: red;

  @media screen {
    background-color: green;
  }
}

Nach dem Parsen wird dieses Snippet in Chrome vor Version 130 so angezeigt:

#foo,
.foo,
.foo::before {
  width: fit-content;
  background-color: red;

  @media screen {
    & {
      background-color: green;
    }
  }
}

Das ist ein Problem, weil die verschachtelte CSSRule mit der &-Auswahl:

  • Sie wird auf :is(#foo, .foo) reduziert und .foo::before wird dabei aus der Auswahlliste entfernt.
  • Hat eine Spezifität von (1,0,0), was ein späteres Überschreiben erschwert.

Sie können dies prüfen, indem Sie sich ansehen, in welche Form die Regel serialisiert wird:

↳ 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

Visuell bedeutet das auch, dass die background-color von .foo::before red statt green ist.

Ein weiterer Ansatz, den die CSS-Arbeitsgruppe untersuchte, bestand darin, alle verschachtelten Deklarationen in eine @nest-Regel einzubetten. Dieser Vorschlag wurde abgelehnt, da dies die Entwicklererfahrung verschlechtern würde.

Die CSSNestedDeclarations-Benutzeroberfläche

Die CSS-Arbeitsgruppe hat sich für die Einführung der Regel für verschachtelte Deklarationen entschieden.

Diese Regel für verschachtelte Deklarationen wird in Chrome ab Version 130 implementiert.

Unterstützte Browser

  • Chrome: 130.
  • Edge: 130.
  • Firefox: 132.
  • Safari: Nicht unterstützt.

Mit der Einführung der Regel für verschachtelte Deklarationen wird der CSS-Parser so geändert, dass aufeinanderfolgende direkt verschachtelte Deklarationen automatisch in einer CSSNestedDeclarations-Instanz verpackt werden. Bei der Serialisierung wird diese CSSNestedDeclarations-Instanz in der cssRules-Eigenschaft der CSSStyleRule gespeichert.

Sehen wir uns noch einmal die folgende CSSStyleRule als Beispiel an:

.foo {
  width: fit-content;

  @media screen {
    background-color: red;
  }
    
  background-color: green;
}

In Chrome 130 oder höher sieht die Serialisierung so aus:

↳ 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

Da die CSSNestedDeclarations-Regel in CSSRuleList endet, kann der Parser die Position der background-color: green-Deklaration nach der background-color: red-Deklaration beibehalten, die Teil der CSSMediaRule ist.

Darüber hinaus führt eine CSSNestedDeclarations-Instanz keine der bösartigen Nebeneffekte auf die andere, die jetzt verworfen wurden und dadurch verursacht werden können: Die verschachtelte Deklarationsregel stimmt genau mit denselben Elementen und Pseudoelementen wie die übergeordnete Stilregel überein – mit der gleichen Spezifität.

Als Nachweis können Sie cssText der CSSStyleRule zurücklesen. Dank der Regel für verschachtelte Deklarationen ist es mit dem Eingabe-CSS identisch:

.foo {
  width: fit-content;

  @media screen {
    background-color: red;
  }
    
  background-color: green;
}

Was bedeutet das für dich?

Das bedeutet, dass das CSS-Verschachteln seit Chrome 130 viel besser funktioniert. Es bedeutet aber auch, dass Sie möglicherweise einen Teil Ihres Codes überarbeiten müssen, wenn Sie reine Deklarationen mit verschachtelten Regeln vermischt haben.

Sehen wir uns das folgende Beispiel an, in dem das tolle @starting-style

/* This does not work in Chrome 130 */
#mypopover:popover-open {
  @starting-style {
    opacity: 0;
    scale: 0.5;
  }

  opacity: 1;
  scale: 1;
}

Vor Chrome 130 wurden diese Deklarationen vorangestellt. Die opacity: 1;- und scale: 1;-Deklarationen werden in CSSStyleRule.style eingefügt, gefolgt von einer CSSStartingStyleRule (für die @starting-style-Regel) in CSSStyleRule.cssRules.

Ab Chrome 130 werden die Deklarationen nicht mehr hochgezogen und es gibt zwei verschachtelte CSSRule-Objekte in CSSStyleRule.cssRules. Der Reihe nach: ein CSSStartingStyleRule (für die @starting-style-Regel) und ein CSSNestedDeclarations, der die opacity: 1; scale: 1;-Deklarationen enthält.

Aufgrund dieses geänderten Verhaltens werden die @starting-style-Deklarationen durch die in der CSSNestedDeclarations-Instanz enthaltenen Deklarationen überschrieben, wodurch die Eintraganimation entfernt wird.

Um den Code zu korrigieren, muss der @starting-style-Block nach den regulären Deklarationen stehen. Beispiel:

/* This works in Chrome 130 */
#mypopover:popover-open {
  opacity: 1;
  scale: 1;

  @starting-style {
    opacity: 0;
    scale: 0.5;
  }
}

Wenn Sie bei der Verwendung von CSS-Verschachtelung Ihre verschachtelten Deklarationen über den verschachtelten Regeln platzieren, funktioniert Ihr Code meistens in allen Versionen aller Browser, die CSS-Verschachtelung unterstützen.

Wenn Sie die Verfügbarkeit von CSSNestedDeclarations prüfen möchten, können Sie das folgende JavaScript-Snippet verwenden:

if (!("CSSNestedDeclarations" in self && "style" in CSSNestedDeclarations.prototype)) {
  // CSSNestedDeclarations is not available
}