Apprivoiser la cascade avec les sélecteurs BEM et CSS modernes PlatoBlockchain Data Intelligence. Recherche verticale. Aï.

Apprivoiser la cascade avec BEM et les sélecteurs CSS modernes

BEM. Comme apparemment toutes les techniques dans le monde du développement front-end, rédaction de CSS au format BEM peut être polarisant. Mais c'est - du moins dans ma bulle Twitter - l'une des méthodologies CSS les plus appréciées.

Personnellement, je pense que BEM est bon, et je pense que vous devriez l'utiliser. Mais je comprends aussi pourquoi vous pourriez ne pas le faire.

Quelle que soit votre opinion sur BEM, il offre plusieurs avantages, le plus important étant qu'il permet d'éviter les conflits de spécificité dans la CSS Cascade. En effet, s'ils sont utilisés correctement, tous les sélecteurs écrits au format BEM doivent avoir le même score de spécificité (0,1,0). J'ai conçu le CSS pour de nombreux sites Web à grande échelle au fil des ans (pensez au gouvernement, aux universités et aux banques), et c'est sur ces projets plus importants que j'ai trouvé que BEM brille vraiment. L'écriture de CSS est beaucoup plus amusante lorsque vous êtes sûr que les styles que vous écrivez ou modifiez n'affectent pas une autre partie du site.

Il existe en fait des exceptions où il est jugé tout à fait acceptable d'ajouter de la spécificité. Par exemple : le :hover ainsi que :focus pseudo-classes. Ceux-ci ont un score de spécificité de 0,2,0. Un autre est les pseudo-éléments - comme ::before ainsi que ::after — qui ont un score de spécificité de 0,1,1. Pour le reste de cet article cependant, supposons que nous ne voulons pas d'autre dérive de spécificité. 🤓

Mais je ne suis pas vraiment là pour vous vendre sur BEM. Au lieu de cela, je veux parler de la façon dont nous pouvons l'utiliser avec les sélecteurs CSS modernes - pensez :is(), :has(), :where(), etc. — pour gagner même PLUS contrôle de la cascade.

Qu'en est-il des sélecteurs CSS modernes ?

La Spécification des sélecteurs CSS niveau 4 nous donne de nouvelles et puissantes façons de sélectionner des éléments. Certains de mes favoris incluent :is(), :where()et :not(), dont chacun est pris en charge par tous les navigateurs modernes et peut être utilisé en toute sécurité sur presque tous les projets de nos jours.

:is() ainsi que :where() sont fondamentalement la même chose, sauf en ce qui concerne leur impact sur la spécificité. Spécifiquement, :where() a toujours un score de spécificité de 0,0,0. Ouais, même :where(button#widget.some-class) n'a aucune spécificité. Parallèlement, la spécificité de :is() est l'élément de sa liste d'arguments avec la plus grande spécificité. Donc, nous avons déjà une distinction en cascade entre deux sélecteurs modernes avec lesquels nous pouvons travailler.

L'incroyablement puissant :has() la pseudo-classe relationnelle est aussi prend rapidement en charge les navigateurs (et est la plus grande nouvelle fonctionnalité de CSS depuis Grille, à mon humble avis). Cependant, au moment de la rédaction, la prise en charge du navigateur pour :has() n'est pas encore assez bon pour être utilisé en production.

Laissez-moi coller une de ces pseudo-classes dans mon BEM et…

/* ❌ specificity score: 0,2,0 */
.something:not(.something--special) {
  /* styles for all somethings, except for the special somethings */
}

Oups ! Vous voyez ce score de spécificité ? N'oubliez pas qu'avec BEM, nous souhaitons idéalement que nos sélecteurs aient tous un score de spécificité de 0,1,0. Pourquoi est-ce 0,2,0 mal? Considérez ce même exemple, développé :

.something:not(.something--special) {
  color: red;
}
.something--special {
  color: blue;
}

Même si le deuxième sélecteur est le dernier dans l'ordre des sources, la spécificité supérieure du premier sélecteur (0,2,0) gagne, et la couleur de .something--special les éléments seront réglés sur red. Autrement dit, en supposant que votre BEM est écrit correctement et que l'élément sélectionné a à la fois le .something classe de base et .something--special classe de modificateur qui lui est appliquée dans le HTML.

Utilisées avec insouciance, ces pseudo-classes peuvent avoir un impact sur la cascade de manière inattendue. Et ce sont ces types d'incohérences qui peuvent créer des maux de tête sur toute la ligne, en particulier sur des bases de code plus grandes et plus complexes.

Merde. Et maintenant?

Rappelez-vous ce que je disais à propos de :where() et le fait que sa spécificité soit nulle ? Nous pouvons utiliser cela à notre avantage :

/* ✅ specificity score: 0,1,0 */
.something:where(:not(.something--special)) {
  /* etc. */
}

La première partie de ce sélecteur (.something) obtient son score de spécificité habituel de 0,1,0. Mais :where() - et tout ce qu'il contient - a une spécificité de 0, ce qui n'augmente pas davantage la spécificité du sélecteur.

:where() nous permet de nicher

Les gens qui ne se soucient pas autant que moi de la spécificité (et c'est probablement beaucoup de gens, pour être juste) ont plutôt bien réussi en matière de nidification. Avec quelques coups de clavier insouciants, nous pouvons nous retrouver avec un CSS comme celui-ci (notez que j'utilise Sass par souci de brièveté):

.card { ... }

.card--featured {
  /* etc. */  
  .card__title { ... }
  .card__title { ... }
}

.card__title { ... }
.card__img { ... }

Dans cet exemple, nous avons un .card composant. Lorsqu'il s'agit d'une carte "en vedette" (en utilisant le .card--featured classe), le titre et l'image de la carte doivent être stylisés différemment. Mais, comme nous maintenant savoir, le code ci-dessus se traduit par un score de spécificité qui est incompatible avec le reste de notre système.

Un inconditionnel de la spécificité aurait pu faire ceci à la place :

.card { ... }
.card--featured { ... }
.card__title { ... }
.card__title--featured { ... }
.card__img { ... }
.card__img--featured { ... }

Ce n'est pas si mal, non ? Franchement, c'est beau CSS.

Il y a cependant un inconvénient dans le HTML. Les auteurs chevronnés de BEM sont probablement douloureusement conscients de la logique de modèle maladroite qui est nécessaire pour appliquer conditionnellement des classes de modificateurs à plusieurs éléments. Dans cet exemple, le modèle HTML doit ajouter conditionnellement le --featured classe de modificateur à trois éléments (.card, .card__titleet .card__img) mais probablement encore plus dans un exemple réel. C'est beaucoup de if Déclarations.

La :where() selector peut nous aider à écrire beaucoup moins de logique de modèle - et moins de classes BEM pour démarrer - sans ajouter au niveau de spécificité.

.card { ... }
.card--featured { ... }

.card__title { ... }
:where(.card--featured) .card__title { ... }

.card__img { ... }
:where(.card--featured) .card__img { ... }

Voici la même chose mais en Sass (notez la fin esperluette):

.card { ... }
.card--featured { ... }
.card__title { 
  /* etc. */ 
  :where(.card--featured) & { ... }
}
.card__img { 
  /* etc. */ 
  :where(.card--featured) & { ... }
}

Que vous deviez ou non opter pour cette approche plutôt que d'appliquer des classes de modificateurs aux divers éléments enfants est une question de préférence personnelle. Mais au moins :where() nous laisse le choix maintenant !

Qu'en est-il du HTML non-BEM ?

Nous ne vivons pas dans un monde parfait. Parfois, vous devez gérer du HTML qui échappe à votre contrôle. Par exemple, un script tiers qui injecte du code HTML que vous devez styliser. Ce balisage n'est souvent pas écrit avec les noms de classe BEM. Dans certains cas, ces styles n'utilisent pas du tout des classes mais des identifiants !

Une fois de plus, :where() a notre dos. Cette solution est légèrement hacky, car nous devons référencer la classe d'un élément quelque part plus haut dans l'arborescence DOM dont nous savons qu'il existe.

/* ❌ specificity score: 1,0,0 */
#widget {
  /* etc. */
}

/* ✅ specificity score: 0,1,0 */
.page-wrapper :where(#widget) {
  /* etc. */
}

Faire référence à un élément parent semble cependant un peu risqué et restrictif. Que se passe-t-il si cette classe parent change ou n'existe pas pour une raison quelconque ? Une meilleure solution (mais peut-être tout aussi hacky) serait d'utiliser :is() Au lieu. Rappelez-vous, la spécificité de :is() est égal au sélecteur le plus spécifique de sa liste de sélecteurs.

Ainsi, au lieu de faire référence à une classe dont nous savons (ou espérons !) qu'elle existe avec :where(), comme dans l'exemple ci-dessus, nous pourrions référencer une classe composée et la Étiquette.

/* ✅ specificity score: 0,1,0 */
:is(.dummy-class, body) :where(#widget) {
  /* etc. */
}

Le toujours présent body nous aidera à choisir notre #widget élément, et la présence de .dummy-class classe à l'intérieur du même :is() donne l' body sélecteur le même score de spécificité qu'une classe (0,1,0)… et l'utilisation de :where() assure que le sélecteur ne devienne pas plus précis que cela.

C'est tout!

C'est ainsi que nous pouvons tirer parti des fonctionnalités modernes de gestion de la spécificité du :is() ainsi que :where() pseudo-classes aux côtés de la prévention des collisions de spécificité que nous obtenons lors de l'écriture CSS dans un format BEM. Et dans un avenir pas trop lointain, une fois :has() gagne le support de Firefox (il est actuellement pris en charge derrière un indicateur au moment de la rédaction), nous voudrons probablement l'associer à :where() pour annuler sa spécificité.

Que vous optiez pour la dénomination BEM ou non, j'espère que nous pourrons convenir qu'avoir une cohérence dans la spécificité du sélecteur est une bonne chose !

Horodatage:

Plus de Astuces CSS