Les préprocesseurs connaissent les CSS imbriqués depuis les années 2000
La fonctionnalité, qui a été proposée pour la première fois en 2011 dans une liste de diffusion W3C par l'éditeur en charge, Tab Atkins, un développeur de Google, sous le nom de « Selector Nesting », apporte pour la première fois la possibilité d'imbriquer des sélecteurs CSS en natif, c'est-à-dire sans avoir besoin d'autres outils.
Les développeurs qui travaillent déjà avec des préprocesseurs tels que Sass, Less, SCSS ou, plus récemment, PostCSS utilisent l'imbrication de sélecteurs dans la syntaxe interne des outils de préprocesseur depuis 2007. Il est considéré comme l'un des principaux arguments en faveur de l'utilisation de Sass and Co. Pour une utilisation sur le Web, les préprocesseurs génèrent un CSS conforme à la norme. À l'avenir, les règles d'imbrication devraient appartenir à ce CSS conforme à la norme.
Le CSSWG décrit la fonctionnalité comme suit : « Ce module décrit la prise en charge de l'imbrication d'une règle de style dans une autre règle de style afin que le sélecteur de la règle interne puisse référencer les éléments de la règle externe. Avec cette fonction, les styles associés peuvent être résumés dans une structure unique au sein du document CSS, ce qui améliore la lisibilité grâce à une hiérarchie visuelle et la maintenabilité en évitant les répétitions. »
Motivation
Le CSS pour les pages Web, même modérément compliquées, comprend souvent de nombreuses duplications dans le but de styliser le contenu associé. Par exemple, voici une partie du balisage CSS pour une version du module [CSS-COLOR-3]*:
Code CSS : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | table.colortable td { text-align:center; } table.colortable td.c { text-transform:uppercase; } table.colortable td:first-child, table.colortable td:first-child+td { border:1px solid black; } table.colortable th { text-align:center; background:black; color:white; } |
L'imbrication permet de regrouper des règles de style associées, comme ceci*:
Code CSS : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 | table.colortable { & td { text-align:center; &.c { text-transform:uppercase } &:first-child, &:first-child + td { border:1px solid black } } & th { text-align:center; background:black; color:white; } } |
Outre la suppression des doublons, le regroupement des règles associées améliore la lisibilité et la maintenabilité du CSS résultant.
Imbrication directe et indirecte
Les règles de style peuvent être imbriquées dans d'autres règles de style. Ces règles de style imbriquées agissent exactement comme des règles de style ordinaires (associant des propriétés à des éléments via des sélecteurs) mais elles « héritent » du contexte de sélecteur de leur règle parent, leur permettant de s'appuyer davantage sur le sélecteur du parent sans avoir à le répéter, éventuellement plusieurs fois.
Il existe deux syntaxes étroitement liées pour créer des règles de style imbriquées*:
- Imbrication directe, où la règle de style imbriquée est écrite normalement à l'intérieur de la règle parent, mais avec l'exigence que le sélecteur de la règle de style imbriqué soit préfixé par imbrication.
- La règle @nest, qui impose moins de contraintes sur le sélecteur de la règle de style imbriqué.
Mis à part la légère différence dans la façon dont elles sont écrites, les deux méthodes sont exactement équivalentes en termes de fonctionnalités.
Imbrication directe
Une règle de style peut être directement imbriquée dans une autre règle de style si son sélecteur est préfixé par imbrication.
Pour être préfixé par imbrication, un sélecteur d'imbrication doit être le premier sélecteur simple dans le premier sélecteur composé du sélecteur. Si le sélecteur est une liste de sélecteurs, chaque sélecteur complexe de la liste doit être préfixé par imbrication pour que le sélecteur dans son ensemble soit préfixé par imbrication.
Par exemple, les imbrications suivantes sont valides*:
Code CSS : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | /* & can be used on its own */ .foo { color: blue; & > .bar { color: red; } } /* equivalent to .foo { color: blue; } .foo > .bar { color: red; } */ /* or in a compound selector, refining the parents selector */ .foo { color: blue; &.bar { color: red; } } /* equivalent to .foo { color: blue; } .foo.bar { color: red; } */ /* multiple selectors in the list must all start with & */ .foo, .bar { color: blue; & + .baz, &.qux { color: red; } } /* equivalent to .foo, .bar { color: blue; } :is(.foo, .bar) + .baz, :is(.foo, .bar).qux { color: red; } */ /* & can be used multiple times in a single selector */ .foo { color: blue; & .bar & .baz & .qux { color: red; } } /* equivalent to .foo { color: blue; } .foo .bar .foo .baz .foo .qux { color: red; } */ /* Somewhat silly, but can be used all on its own, as well. */ .foo { color: blue; & { padding: 2ch; } } /* equivalent to .foo { color: blue; } .foo { padding: 2ch; } // or .foo { color: blue; padding: 2ch; } */ /* Again, silly, but can even be doubled up. */ .foo { color: blue; && { padding: 2ch; } } /* equivalent to .foo { color: blue; } .foo.foo { padding: 2ch; } */ /* The parent selector can be arbitrarily complicated */ .error, #404 { &:hover > .baz { color: red; } } /* equivalent to :is(.error, #404):hover > .baz { color: red; } */ /* As can the nested selector */ .foo { &:is(.bar, &.baz) { color: red; } } /* equivalent to .foo:is(.bar, .foo.baz) { color: red; } */ /* Multiple levels of nesting "stack up" the selectors */ figure { margin: 0; & > figcaption { background: hsl(0 0% 0% / 50%); & > p { font-size: .9rem; } } } /* equivalent to figure { margin: 0; } figure > figcaption { background: hsl(0 0% 0% / 50%); } figure > figcaption > p { font-size: .9rem; } */ |
Mais ces imbrications ne sont pas valides :
Code CSS : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | /* No & at all */ .foo { color: blue; .bar { color: red; } } /* & isnt the first simple selector */ .foo { color: blue; .bar& { color: red; } } /* & isnt the first selector of every one in the list */ .foo, .bar { color: blue; & + .baz, .qux { color: red; } } |
Le dernier exemple ici n'est pas techniquement ambigu, car le sélecteur dans son ensemble commence par un &, mais c'est un risque d'édition - si la règle est remaniée pour supprimer le premier sélecteur ou réorganiser les sélecteurs dans la liste, ce qui normalement serait toujours valide, il en résulterait un sélecteur invalide désormais ambigu.
Certains outils de génération CSS concaténeront des sélecteurs tels que des chaînes, permettant aux auteurs de créer un seul sélecteur simple à travers les niveaux d'imbrication. Ceci est parfois utilisé par des méthodes d'organisation de sélecteurs comme BEM pour réduire la répétition dans un fichier, lorsque les sélecteurs eux-mêmes ont une répétition importante en interne.
Par exemple, si un composant utilise la classe .foo et qu'un composant imbriqué utilise .foo__bar, vous pouvez l'écrire dans Sass comme suit*:
Code CSS : | Sélectionner tout |
1 2 3 4 5 6 7 8 | .foo { color: blue; &__bar { color: red; } } /* In Sass, this is equivalent to .foo { color: blue; } .foo__bar { color: red; } */ |
Malheureusement, cette méthode est incompatible avec la syntaxe du sélecteur en général et nécessite au mieux des heuristiques adaptées aux pratiques d'écriture de sélecteur pour reconnaître quand l'auteur le souhaite, ou quand l'auteur essaye d'ajouter un sélecteur de type dans la règle imbriquée. __bar, par exemple, est un nom d'élément personnalisé valide en HTML.
En tant que tel, CSS ne peut pas faire cela ; les composants de sélecteur imbriqués sont interprétés seuls et non "concaténés":
Code CSS : | Sélectionner tout |
1 2 3 4 5 6 7 8 | .foo { color: blue; &__bar { color: red; } } /* In CSS, this is instead equivalent to .foo { color: blue; } __bar.foo { color: red; } */ |
La règle d'imbrication*: @nest
Bien que l'imbrication directe ait l'air agréable, elle est quelque peu fragile. Certains sélecteurs d'imbrication valides, comme .foo &, ne sont pas autorisés, et la modification du sélecteur de certaines manières peut rendre la règle invalide de manière inattendue. De plus, certains auteurs trouvent que l'imbrication est difficile à distinguer visuellement des déclarations environnantes.
Pour aider à résoudre tous ces problèmes, cette spécification définit la règle @nest, qui impose moins de restrictions sur la manière d'imbriquer validement les règles de style. Sa syntaxe est : @nest = @nest <selector-list> { <style-block> }.
La règle @nest n'est valide qu'à l'intérieur d'une règle de style. Si elle est utilisée dans un autre contexte (en particulier, au niveau supérieur d'une feuille de style), la règle n'est pas valide.
La règle @nest fonctionne de la même manière qu'une règle de style imbriquée*: elle commence par un sélecteur et contient un bloc de déclarations qui s'appliquent aux éléments auxquels le sélecteur correspond. Ce bloc est traité de la même manière qu'un bloc de règle de style, donc tout ce qui est valide dans une règle de style (comme des règles @nest supplémentaires) est également valide ici.
La seule différence entre @nest et une règle de style directement imbriquée est que le sélecteur utilisé dans une règle @nest est moins contraint : il doit seulement être nest-containing, ce qui signifie qu'il contient un sélecteur d'imbrication quelque part, plutôt que de l'exiger être au début de chaque sélecteur. Une liste de sélecteurs contient des imbrications si tous ses sélecteurs complexes individuels contiennent des imbrications.
Tout ce que vous pouvez faire avec l'imbrication directe, vous pouvez le faire avec une règle @nest, donc ce qui suit est valide*:
Code CSS : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 | .foo { color: red; @nest & > .bar { color: blue; } } /* equivalent to .foo { color: red; } .foo > .bar { color: blue; } */ |
Mais @nest autorise les sélecteurs qui ne commencent pas par un &, les éléments suivants sont donc également valides*:
Code CSS : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | .foo { color: red; @nest .parent & { color: blue; } } /* equivalent to .foo { color: red; } .parent .foo { color: blue; } */ .foo { color: red; @nest :not(&) { color: blue; } } /* equivalent to .foo { color: red; } :not(.foo) { color: blue; } */ |
Mais les éléments suivants ne sont pas valides*:
Code CSS : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | .foo { color: red; @nest .bar { color: blue; } } /* Invalid because theres no nesting selector */ .foo { color: red; @nest & .bar, .baz { color: blue; } } /* Invalid because not all selectors in the list contain a nesting selector */ |
Les règles de style directement imbriquées et les règles @nest peuvent être mélangées arbitrairement. Par exemple:
Code CSS : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 | .foo { color: blue; @nest .bar & { color: red; &.baz { color: green; } } } /* equivalent to .foo { color: blue; } .bar .foo { color: red; } .bar .foo.baz { color: green; } |
En somme
La spécification prévoit une imbrication directe et indirecte, la variante indirecte étant recommandée car soumise à moins de restrictions et est également plus facile à distinguer visuellement dans le code. L'imbrication directe doit être effectuée en plaçant une esperluette (&), le véritable sélecteur d'imbrication, devant le sélecteur à imbriquer. Les utilisateurs de préprocesseurs connaissent le & comme « Sélecteur parent » ou « Parent Combinator ».
Avec l'imbrication indirecte, la règle précédente est @nest suivi du sélecteur d'imbrication &. Fondamentalement, seul l'effort de frappe inférieur plaide en faveur de l'utilisation du & seul. Sinon @nest peut faire tout ce qui serait également possible avec &.
Le sélecteur d'imbrication & ne peut être utilisé valablement que là où il apparaît en première position. Cela est dû au processus d'analyse du navigateur. Il parcourt hiérarchiquement le code et ne peut pas faire la distinction entre les propriétés et les sélecteurs si les noms sont identiques. Si le sélecteur d'imbrication venait après lui, il serait tout simplement trop tard pour être utile à une différenciation.
Il est également possible de faire un mix d'imbrication directe et indirecte.
Source : W3C