Publié le 8 octobre 2024
Pour corriger certaines bizarreries liées à l'imbrication CSS, le groupe de travail CSS a décidé d'ajouter l'interface CSSNestedDeclarations
à la spécification d'imbrication CSS. Avec cette addition, les déclarations qui suivent les règles de style ne sont plus déplacées vers le haut, entre autres améliorations.
Ces modifications sont disponibles dans Chrome à partir de la version 130 et peuvent être testées dans Firefox Nightly 132 et Safari Technology Preview 204.
Navigateurs pris en charge
Problème d'imbrication CSS sans CSSNestedDeclarations
L'un des pièges de l'imbrication CSS est que, à l'origine, l'extrait de code suivant ne fonctionne pas comme vous pourriez le penser:
.foo {
width: fit-content;
@media screen {
background-color: red;
}
background-color: green;
}
En examinant le code, vous pourriez supposer que l'élément <div class=foo>
possède un background-color
green
, car la déclaration background-color: green;
vient en dernier. Ce n'est toutefois pas le cas dans Chrome avant la version 130. Dans ces versions, qui ne sont pas compatibles avec CSSNestedDeclarations
, le background-color
de l'élément est red
.
Après avoir analysé la règle réelle, Chrome avant la version 130 utilise la règle suivante:
.foo {
width: fit-content;
background-color: green;
@media screen {
& {
background-color: red;
}
}
}
Le CSS après l'analyse a subi deux modifications:
background-color: green;
a été déplacé vers le haut pour rejoindre les deux autres déclarations.- Le
CSSMediaRule
imbriqué a été réécrit pour encapsuler ses déclarations dans unCSSStyleRule
supplémentaire à l'aide du sélecteur&
.
Un autre changement typique que vous verriez ici est les propriétés de suppression de l'analyseur qu'il n'est pas compatible.
Vous pouvez inspecter le "CSS après l'analyse" par vous-même en lisant à nouveau le cssText
à partir du CSSStyleRule
.
Essayez-le vous-même dans ce playground interactif:
Pourquoi ce CSS est-il réécrit ?
Pour comprendre pourquoi cette réécriture interne s'est produite, vous devez comprendre comment cet élément CSSStyleRule
est représenté dans le modèle d'objet CSS (CSSOM).
Dans Chrome version antérieure à 130, l'extrait CSS partagé précédemment se sérialise comme suit:
↳ 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
Parmi toutes les propriétés d'un CSSStyleRule
, les deux suivantes sont pertinentes dans ce cas:
- La propriété
style
, qui est une instanceCSSStyleDeclaration
représentant les déclarations. - La propriété
cssRules
, qui est unCSSRuleList
contenant tous les objetsCSSRule
imbriqués.
Étant donné que toutes les déclarations de l'extrait CSS se retrouvent dans la propriété style
de CSStyleRule
, des informations sont perdues. En examinant la propriété style
, il n'est pas clair que background-color: green
a été déclarée après CSSMediaRule
imbriquée.
↳ CSSStyleRule
.type = STYLE_RULE
.selectorText = ".foo"
.style (CSSStyleDeclaration, 2) =
- width: fit-content
- background-color: green
.cssRules (CSSRuleList, 1) =
↳ …
Cela pose problème, car pour qu'un moteur CSS fonctionne correctement, il doit pouvoir distinguer les propriétés qui apparaissent au début du contenu d'une règle de style de celles qui apparaissent entremêlées avec d'autres règles.
Quant aux déclarations dans CSSMediaRule
, elles sont soudainement encapsulées dans un CSSStyleRule
, car CSSMediaRule
n'a pas été conçu pour contenir des déclarations.
Étant donné que CSSMediaRule
peut contenir des règles imbriquées (accessibles via sa propriété cssRules
), les déclarations sont automatiquement encapsulées dans 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
Comment résoudre ce problème ?
Le groupe de travail CSS a examiné plusieurs options pour résoudre ce problème.
L'une des solutions suggérées consistait à encapsuler toutes les déclarations nues dans un CSSStyleRule
imbriqué avec le sélecteur d'imbrication (&
). Cette idée a été abandonnée pour diverses raisons, y compris les effets secondaires indésirables suivants du désucrage de &
en :is(…)
:
- Cela a un effet sur la spécificité. En effet,
:is()
reprend la spécificité de son argument le plus spécifique. - Il ne fonctionne pas bien avec les pseudo-éléments du sélecteur externe d'origine. En effet,
:is()
n'accepte pas les pseudo-éléments dans son argument de liste de sélecteur.
Prenons l'exemple suivant :
#foo, .foo, .foo::before {
width: fit-content;
background-color: red;
@media screen {
background-color: green;
}
}
Après l'analyse, cet extrait devient le suivant dans Chrome 130 et versions antérieures:
#foo,
.foo,
.foo::before {
width: fit-content;
background-color: red;
@media screen {
& {
background-color: green;
}
}
}
Cela pose problème, car l'CSSRule
imbriquée avec le sélecteur &
:
- Aplatit jusqu'à
:is(#foo, .foo)
, en supprimant.foo::before
de la liste du sélecteur. - La valeur de spécificité est
(1,0,0)
, ce qui rend plus difficile l'écrasement ultérieur.
Pour vérifier cela, inspectez ce que la règle sérialise:
↳ 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
Visuellement, cela signifie également que le background-color
de .foo::before
est red
au lieu de green
.
Le groupe de travail CSS a également envisagé de vous demander d'encapsuler toutes les déclarations imbriquées dans une règle @nest
. Cette option a été ignorée en raison de la régression de l'expérience développeur que cela entraînerait.
Présentation de l'interface CSSNestedDeclarations
La solution adoptée par le groupe de travail CSS est l'introduction de la règle des déclarations imbriquées.
Cette règle de déclaration imbriquée est implémentée dans Chrome à partir de la version 130.
Navigateurs pris en charge
L'introduction de la règle des déclarations imbriquées modifie l'analyseur CSS pour qu'il encapsule automatiquement les déclarations imbriquées directement consécutives dans une instance CSSNestedDeclarations
. Lors de la sérialisation, cette instance CSSNestedDeclarations
se retrouve dans la propriété cssRules
de CSSStyleRule
.
Prenons à nouveau l'exemple de fichier CSSStyleRule
suivant:
.foo {
width: fit-content;
@media screen {
background-color: red;
}
background-color: green;
}
Lorsqu'il est sérialisé dans Chrome 130 ou version ultérieure, il se présente comme suit:
↳ 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
Étant donné que la règle CSSNestedDeclarations
se termine dans CSSRuleList
, l'analyseur peut conserver la position de la déclaration background-color: green
: après la déclaration background-color: red
(qui fait partie de CSSMediaRule
).
De plus, la présence d'une instance CSSNestedDeclarations
n'entraîne aucun des effets secondaires indésirables que les autres solutions potentielles, désormais abandonnées, ont causés: la règle de déclaration imbriquée correspond exactement aux mêmes éléments et pseudo-éléments que sa règle de style parente, avec le même comportement de spécificité.
Pour le prouver, lisez à nouveau le cssText
de l'CSSStyleRule
. Grâce à la règle des déclarations imbriquées, il est identique au CSS d'entrée:
.foo {
width: fit-content;
@media screen {
background-color: red;
}
background-color: green;
}
Ce que cela implique pour vous
Cela signifie que l'imbrication CSS a été considérablement améliorée à partir de Chrome 130. Cela signifie également que vous devrez peut-être revoir certains de vos codes si vous intercalez des déclarations brutes avec des règles imbriquées.
Prenons l'exemple suivant qui utilise la merveilleuse @starting-style
.
/* This does not work in Chrome 130 */
#mypopover:popover-open {
@starting-style {
opacity: 0;
scale: 0.5;
}
opacity: 1;
scale: 1;
}
Avant Chrome 130, ces déclarations étaient hissées. Les déclarations opacity: 1;
et scale: 1;
seront alors insérées dans CSSStyleRule.style
, suivies d'un CSSStartingStyleRule
(représentant la règle @starting-style
) dans CSSStyleRule.cssRules
.
À partir de Chrome 130, les déclarations ne sont plus hissées, et vous obtenez deux objets CSSRule
imbriqués dans CSSStyleRule.cssRules
. Dans l'ordre: un élément CSSStartingStyleRule
(représentant la règle @starting-style
) et un élément CSSNestedDeclarations
contenant les déclarations opacity: 1; scale: 1;
.
En raison de ce changement de comportement, les déclarations @starting-style
sont écrasées par celles contenues dans l'instance CSSNestedDeclarations
, ce qui supprime l'animation d'entrée.
Pour corriger le code, assurez-vous que le bloc @starting-style
vient après les déclarations régulières. Par exemple :
/* This works in Chrome 130 */
#mypopover:popover-open {
opacity: 1;
scale: 1;
@starting-style {
opacity: 0;
scale: 0.5;
}
}
Si vous conservez vos déclarations imbriquées au-dessus des règles imbriquées lorsque vous utilisez l'imbrication CSS, votre code fonctionne essentiellement avec toutes les versions de tous les navigateurs compatibles avec l'imbrication CSS.
Enfin, si vous souhaitez que les fonctionnalités détectent la disponibilité de CSSNestedDeclarations
, vous pouvez utiliser l'extrait JavaScript suivant:
if (!("CSSNestedDeclarations" in self && "style" in CSSNestedDeclarations.prototype)) {
// CSSNestedDeclarations is not available
}