Il nidificazione CSS migliora con CSSNestedDeclarations

Data di pubblicazione: 8 ottobre 2024

Per risolvere alcuni strani problemi di nidificazione del CSS, il gruppo di lavoro CSS ha deciso di aggiungere l'interfaccia CSSNestedDeclarations alla specifica di nidificazione del CSS. Con questa aggiunta, le dichiarazioni che seguono le regole di stile non vengono più spostate verso l'alto, tra altri miglioramenti.

Queste modifiche sono disponibili in Chrome dalla versione 130 e sono pronte per essere testate in Firefox Nightly 132 e Safari Technology Preview 204.

Supporto dei browser

  • Chrome: 130.
  • Edge: 130.
  • Firefox: 132.
  • Safari: non supportato.

Il problema con l'annidamento CSS senza CSSNestedDeclarations

Uno dei problemi con l'annidamento CSS è che, in origine, lo snippet seguente non funziona come potresti inizialmente aspettarti:

.foo {
    width: fit-content;

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

Osservando il codice, si presume che l'elemento <div class=foo> abbia un green background-color perché la dichiarazione background-color: green; è l'ultima. Non è così in Chrome prima della versione 130. In queste versioni, che non supportano CSSNestedDeclarations, il background-color dell'elemento è red.

Dopo l'analisi della regola effettiva, Chrome prima della versione 130 utilizza quanto segue:

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

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

Il CSS dopo l'analisi ha subito due modifiche:

  • Il background-color: green; è stato spostato verso l'alto per unirsi alle altre due dichiarazioni.
  • Il CSSMediaRule nidificato è stato riscritto per racchiudere le relative dichiarazioni in un CSSStyleRule aggiuntivo utilizzando il selettore &.

Un'altra modifica tipica che vedresti in questo caso è l'eliminazione delle proprietà del parser che non supporta.

Puoi controllare il "CSS dopo l'analisi" leggendo cssText da CSSStyleRule.

Prova tu stesso questo playground interattivo:

Perché questo CSS è stato riscritto?

Per capire perché si è verificata questa riscrittura interna, devi capire come questo CSSStyleRule viene rappresentato nel CSS Object Model (CSSOM).

In Chrome prima della versione 130, lo snippet CSS condiviso in precedenza viene serializzato nel seguente modo:

↳ 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

Di tutte le proprietà di un CSSStyleRule, in questo caso sono pertinenti le seguenti due:

  • La proprietà style, che è un'istanza CSSStyleDeclaration che rappresenta le dichiarazioni.
  • La proprietà cssRules, che è un elemento CSSRuleList che contiene tutti gli oggetti CSSRule nidificati.

Poiché tutte le dichiarazioni dello snippet CSS finiscono nella proprietà style di CSStyleRule, si verifica una perdita di informazioni. Quando esamini la proprietà style, non è chiaro che background-color: green sia stato dichiarato dopo CSSMediaRule nidificato.

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

Questo è un problema, perché per il corretto funzionamento di un motore CSS è necessario distinguere le proprietà che appaiono all'inizio dei contenuti di una regola di stile da quelle che appaiono intersecate con altre regole.

Le dichiarazioni all'interno di CSSMediaRule vengono improvvisamente incluse in un elemento CSSStyleRule: questo perché CSSMediaRule non è stato progettato per contenere dichiarazioni.

Poiché CSSMediaRule può contenere regole nidificate, accessibili tramite la relativa proprietà cssRules, le dichiarazioni vengono automaticamente racchiuse in un 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

Come risolvere il problema?

Il gruppo di lavoro CSS ha esaminato diverse opzioni per risolvere il problema.

Una delle soluzioni suggerite era racchiudere tutte le dichiarazioni bare in un CSSStyleRule nidificato con il selettore di nidificazione (&). Questa idea è stata scartata per vari motivi, tra cui i seguenti effetti collaterali indesiderati della desugarizzazione di & in :is(…):

  • Ha un effetto sulla specificità. Questo perché :is() assume la specificità del suo argomento più specifico.
  • Non funziona bene con gli pseudo-elementi nel selettore esterno originale. Questo accade perché :is() non accetta pseudo-elementi nell'argomento dell'elenco di selettori.

Prendi ad esempio quanto segue:

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

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

Dopo l'analisi, lo snippet diventa questo in Chrome prima della versione 130:

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

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

Si tratta di un problema perché il CSSRule nidificato con il selettore &:

  • Appiattisce a :is(#foo, .foo), eliminando .foo::before dall'elenco del selettore.
  • Ha una specificità di (1,0,0) che ne rende più difficile la sovrascrittura in un secondo momento.

Per verificare, controlla a cosa viene serializzata la regola:

↳ 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

Visivamente, significa anche che il background-color di .foo::before è red anziché green.

Un altro approccio esaminato dal gruppo di lavoro CSS è stato quello di racchiudere tutte le dichiarazioni nidificate in una regola @nest. Questa richiesta è stata ignorata a causa del peggioramento dell'esperienza dello sviluppatore che ne sarebbe derivato.

Presentazione dell'interfaccia di CSSNestedDeclarations

La soluzione scelta dal gruppo di lavoro CSS è l'introduzione della regola delle dichiarazioni nidificate.

Questa regola di dichiarazioni nidificate è implementata in Chrome a partire dalla versione 130.

Supporto dei browser

  • Chrome: 130.
  • Edge: 130.
  • Firefox: 132.
  • Safari: non supportato.

L'introduzione della regola delle dichiarazioni nidificate modifica il parser CSS in modo che inserisca automaticamente le dichiarazioni consecutive nidificate direttamente in un'istanza CSSNestedDeclarations. Se serializzata, questa istanza CSSNestedDeclarations finisce nella proprietà cssRules di CSSStyleRule.

Prendiamo di nuovo il seguente CSSStyleRule come esempio:

.foo {
  width: fit-content;

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

Se viene serializzato in Chrome 130 o versioni successive, ha il seguente aspetto:

↳ 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

Poiché la regola CSSNestedDeclarations termina in CSSRuleList, l'analizzatore è in grado di mantenere la posizione della dichiarazione background-color: green: dopo la dichiarazione background-color: red (che fa parte di CSSMediaRule).

Inoltre, la presenza di un'istanza CSSNestedDeclarations non introduce nessuno dei cattivi effetti collaterali delle altre potenziali soluzioni, ora scartate, causate: la regola delle dichiarazioni nidificate corrisponde esattamente agli stessi elementi e pseudo-elementi della regola di stile padre, con lo stesso comportamento di specificità.

A riprova di ciò, ti chiedo di leggere il cssText del CSSStyleRule. Grazie alla regola delle dichiarazioni nidificate, è uguale al CSS di input:

.foo {
  width: fit-content;

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

Cosa comporta tutto ciò per te

Ciò significa che l'annidamento CSS è stato notevolmente migliorato a partire da Chrome 130. Tuttavia, significa anche che potresti dover rivedere parte del codice se intercalassi dichiarazioni semplici con regole nidificate.

Prendi l'esempio seguente che utilizza la meravigliosa @starting-style

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

  opacity: 1;
  scale: 1;
}

Prima di Chrome 130, queste dichiarazioni venivano sollevate. Le dichiarazioni opacity: 1; e scale: 1; verranno inserite in CSSStyleRule.style, seguite da un CSSStartingStyleRule (che rappresenta la regola @starting-style) in CSSStyleRule.cssRules.

A partire da Chrome 130, le dichiarazioni non vengono più sollevate e ti ritrovi con due oggetti CSSRule nidificati in CSSStyleRule.cssRules. In ordine: un CSSStartingStyleRule (che rappresenta la regola @starting-style) e un CSSNestedDeclarations contenente le dichiarazioni opacity: 1; scale: 1;.

A causa di questo comportamento modificato, le dichiarazioni @starting-style vengono sovrascritte da quelle contenute nell'istanza CSSNestedDeclarations, rimuovendo così l'animazione di entrata.

Per correggere il codice, assicurati che il blocco @starting-style venga dopo le normali dichiarazioni. In questo modo:

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

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

Se mantieni le dichiarazioni nidificate sopra le regole nidificate quando utilizzi il nesting CSS, il tuo codice funziona per lo più bene con tutte le versioni di tutti i browser che supportano il nesting CSS.

Infine, se vuoi funzionalità per rilevare la disponibilità di CSSNestedDeclarations, puoi utilizzare il seguente snippet JavaScript:

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