L'héritage avec @extend

Pour inaugurer cette seconde partie consacrée à des sujets un peu plus avancés concernant Sass, j’ai décidé de vous parler de la directive @extend. L’héritage n’est pas une notion complexe en soit, mais je n’ai pas souhaité vous en parler avant car on l’utilise assez peu, et que son utilisation est de plus en plus déconseillée. Cependant, je m’en voudrait de ne pas l’évoquer pour autant. On va donc voir en quoi cela consiste, et on verra ensuite pourquoi il est recommandé de s’en passer.

Il est où le grisbi ?

Rassurez-vous, votre grande-tante est encore en vie ! Ici, ce sont des sélecteurs qui héritent. L’héritage est la possibilité pour un élément d’hériter des propriétés d’un autre élément. Prenons par exemple les cartes de notre page Web (représentant un produit, une étape de production ou un avis de client). Elles ont toutes des propriétés en commun, regroupées dans la classe .card, mais ont aussi un certain nombre de propriétés différentes, qui sont réparties entre les classes .product, .step et .client. On peut dire que, conceptuellement, une élément .product, .step ou .client hérite des propriétés de la classe .card.

Pour l’instant, cet héritage est écrit en dur dans le HTML :chaque élément a deux classes. Si jamais on oublie de donner la classe card a un élément product, il ne recevra pas tous les styles nécessaires pour son bon affichage. Sass nous permet de gérer cela autrement, à l’intérieur de la feuille de style, avec la directive @extend. Cette dernière permet de dire qu’un élément X hérite forcément des propriétés d’un élément Y :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
.card {
    ...
    img  {
        ...
    }
}
.product {
    @extend .card;
    min-height: 20rem;
    background-color: #fff;
    color: #222;
    ...
}
.step {
    @extend .card;
}
.client {
    @extend .card;
    background-color: #222;
    color: #fff;
    ...
}

Qu’est-ce que cela va donner en CSS ? En fait Sass va, tout simplement, placer les sélecteurs qui héritent partout où il trouve le sélecteur .card. Ainsi, tous les blocs de propriétés concernant .card seront appliqués aussi à .product, .step et .client :

1
2
3
4
5
6
.card, .product, .step, .client {
    ...
}
.card img, .product img, .step img, .client img {
    ...
}

Et voilà, on n’a donc plus besoin de la classe card dans notre HTML, vu que l’héritage est déjà géré dans le CSS. Un nouvel élément a lui aussi besoin de ressembler à un carte ? Pas de souci, il suffit de le faire hériter lui aussi.

Il est important de noter qu’on peut spécifier n’importe quel sélecteur simple après @extend. Il ne peut donc pas s’agir d’un sélecteur imbriqué du type X Y, X>Y, X+Y et X~Y (les deux derniers étant respectivement les sélecteurs d’adjacence directe et indirecte). Bon, d’un autre côté, si vous souhaitez que .bidule hérite de .machin>p+div~.truc, c’est que vous avez l’esprit légèrement tordu.

Encore plus fort : on peut faire des héritages en chaîne, «en cascade» :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
.message {
border: 2px solid black;
}
.alerte {
    @extend .message;
    color: red;
}
.danger {
    @extend .alerte;
    font-size: xx-large;
}

La classe .danger hérite de la classe .alerte qui hérite de la classe .message !

Les placeholders (un nouvel eldorado ?)

Maintenant que nos classes héritent de .card, on a vu qu’on n’avait plus besoin de mentionner card dans notre HTML. À vrai dire, la classe .card n’est utile que pour Sass, pour faire notre héritage. Depuis sa version 3.2, Sass inclut donc un nouveau type de sélecteurs, les placeholders, ou « @extend-only selectors ». Un placeholder est une classe qui ne sert qu’à l’héritage, et qui disparaitra durant la compilation. Pour transformer une classe en placeholder, ce n’est pas bien compliqué : il suffit de remplacer le . par un %. Ainsi, dans notre exemple :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
%card {
    ...
    img {
        ...
    }
}
.product {
    @extend %card;
    min-height: 20rem;
    background-color: #fff;
    color: #222;
    ...
}
.step {
    @extend %card;
}
.client {
    @extend %card;
    background-color: #222;
    color: #fff;
    ...
}

Et le résultat en CSS ? La même chose qu’auparavant, mais sans .card :

1
2
3
4
5
6
.product, .step, .client {
    ...
}
.product img, .step img, .client img {
    ...
}

On se retrouve donc avec un placeholder, qui n’apparait que dans le fichier SCSS, dont héritent les classes que l’on utilise réellement dans le HTML.

Bonne pratique : Faut-il oublier @extend ?

Mais, n’aurait-on pas pu utiliser un mixin à la place ?

Héhé, excellente question, chers lecteurs, que de nombreuses personnes se sont posées avant vous. Il est vrai qu’à chaque fois qu’on utilise l’héritage (ici avec le placeholder %card), on pourrait aussi faire appel à un mixin (le mixin card). À l’inverse, on aurait aussi pu gérer nos boutons avec l’héritage de placeholders plutôt qu’avec des mixins : on n’a pas accès aux arguments, mais on aurait pu créer un placeholder %button, dont héritent deux placeholders %light-button et dark-button, dont héritent les différentes classes de boutons de notre design. Le résultat est en apparence le même, cependant, on a quelques différences entre les 2 techniques :

  • Il y a des répétitions dans le fichier CSS généré avec des mixins, pas avec l’héritage.
  • On obtient des sélecteurs plus longs avec l’héritage.
  • On ne peux évidemment pas utiliser d’arguments avec l’héritage.
  • L’héritage pose problème à l’intérieur de media-queries, pas les mixins.
  • On obtient parfois des comportements assez inattendus avec l’héritage.

Concernant le premier point, on a vu dans le chapitre sur les mixins que ce n’était pas un réel problème, vu que l’algorithme de gzip gère très bien les répétitions. Concernant le deuxième point, il peut, à grande échelle, influer sur le temps de chargement de la page, mais c’est probablement peu visible1. Le troisième point peut poser problème concernant la ré-utilisabilité du code : un placeholder n’est pas paramétrable, il n’a donc pas vraiment vocation à être réutilisé dans plusieurs projets. Les deux derniers points sont plus complexes, et carrément problématiques. En fait, pour les comprendre, il faut garder en mémoire qu’un placeholder, ce n’est techniquement pas la même chose qu’un mixin, leur fonctionnement étant différent.

Héritage & Media-queries

En effet, lorsqu’une classe .x hérite d’une classe .y, Sass va ajouter le sélecteur de .x partout où il trouvera.y, et il ne fera rien d’autre. Ainsi, imaginons le code suivant :

1
2
3
4
5
6
7
8
%agrume {
    color:red;
}
.mandarine {
    @media (min-width: 720px){
        @extend %agrume;
    }
}

Il est important que vous compreniez que ce code ne peut pas fonctionner. On pourrait s’attendre à obtenir ceci :

1
2
3
4
5
@media (min-width: 720px) {
    .mandarine {
        color:red;
    }
}

Mais ça, c’est ce qui se passerait si on avait utilisé un mixin. Avec un placeholder, c’est très différent : Sass cherche où il peut trouver le sélecteur %agrume pour le remplacer par le sélecteur .mandarine. Sauf qu’il voit bien que l’élément .mandarine est à l’intérieur d’une media-query, alors que %agrume est à l’extérieur de celle-ci. Il ne va pas déplacer l’élément %agrume à l’intérieur de la media-query, parce que ce n’est pas comme cela que fonctionne l’héritage. On a donc droit à une erreur de compilation. En bref, l’héritage et les media-queries ne font pas bon ménage.

Comportements imprévus

Concernant le dernier point, il y a plusieurs choses à savoir. En effet, on pense, à tord, que l’héritage est équivalent à l’utilisation des mixins. Ce qui, régulièrement, peut générer du code superflu et imprévu.

Tout d’abord, rappelez-vous : je vous ai dit dans la première partie qu’on pouvait donner n’importe quel sélecteur simple à @extend. Cela veut dire que, théoriquement, un élément peut hériter d’une balise HTML comme a, strong, h1, etc. Mais ce serait plutôt une mauvaise idée. En effet, il est probable que ces balises existent dans différents contextes sur votre page : dans votre bannière, dans les différentes sections, dans une barre latérale, dans le pied de page… Si la classe .bidon hérite d’une de ces balises, Sass insèrera son nom partout où apparait la balise en question, dans tous les contextes différents où la balise est utilisée. Mais, est-ce vraiment ce que l’on souhaite : la classe .bidon se trouve-t-elle réellement dans chacun de ces contextes ? Parce que, si ce n’est pas le cas, on aura créé des sélecteurs qui n’ont aucune raison d’exister.

Par ailleurs, n’oubliez pas que @extend ne déplace pas le code. Petit exemple :

1
2
3
4
5
6
7
.mandarine {
    @extend %mere;
    color : red;
}
%mandarine {
    color: blue;
}

De quelle couleur est le texte de l’élément .mandarine ? Rouge ? Loupé, il est bleu, parce que la propriété du placeholder %agrume est située en-dessous dans la feuille de style et écrase donc la précédente.

Parlons aussi un peu de la fusion des sélecteurs. Regardez cet exemple :

1
2
3
4
5
6
#div1 #div2 em {
  @extend strong;
}
#div3 #div4 strong {
  font-weight: bold;
}

Comment Sass va-t-il traduire ceci ? J’ai mis en évidence les deux séquences à fusionner. Comme elles n’ont pas d’éléments en commun, Sass créera deux nouveaux sélecteurs : le premier avec les parents de strong avant les parents de em et le second avec les parents de em avant les parents de strong. Voici donc le résultat :

1
2
3
4
5
#div3 #div4 strong,
#div3 #div4 #div1 #div2 em,
#div1 #div2 #div3 #div4 em {
  font-weight: bold;
}

Maintenant, que se passe-t-il lorsque les éléments parents ont au moins un élément en commun ? Faisons le test avec cet exemple :

1
2
3
4
5
6
#commun #div1 em {
  @extend strong;
}
#commun #div2 strong {
  font-weight: bold;
}

Comme vous le voyez, les sélecteurs ont l’élément #commun en commun. Sass, durant la traduction en CSS, fusionnera les deux #commun :

1
2
3
4
5
#commun #div2 strong,
#commun #div2 #div1 em,
#commun #div1 #div2 em {
  font-weight: bold;
}

Là encore, on peut se demander si tous ces sélecteurs générés sont bien utiles, s’ils ciblent bien tous des éléments qui existent réellement dans la page, parce que si ce n’est pas le cas, cela ralentira inutilement l’affichage de la page, ce qui, sur des terminaux peu puissants (GSMs d’entrée de gamme par exemple), se fera sentir.

On oublie l’héritage ?

Je pense que cette section assez théorique devrait vous avoir fait comprendre que l’héritage a de nombreux effets secondaires, alors que les mixins n’ont que des avantages, et sont paramétrables. Plusieurs articles publiés sur des sites spécialisés par des personnes autrement plus qualifiées que moi-même recommandent de proscrire @extend, tout simplement. Après, vous faites ce que vous voulez, vous êtes des grandes personnes. ;)


  1. Encore que… il faudrait vérifier, je suis loin d’être spécialiste du domaine. 


En résumé

  • L’héritage est la posibilité pour un élément d’hériter des propriétés CSS d’un autre élément.
  • Les placeholders sont des sélecteurs dont le nom commence par le symbole % et dont peuvent hériter d’autres éléments. Ils ne servent d’ailleurs qu’à cela car ils n’apparaissent pas ensuite dans le code CSS généré.
  • L’héritage amène parfois à des résultats innatendus et présente de sérieuses limitations. On lui préfère souvent les mixins.

Dans le prochain chapitre, on se lance dans un sujet passionnant, les conditions !