Sass : tout sur @extend

Hugo Giraudel, notre grand spécialiste de Sass, est de retour. Il fait le point de façon précise et claire sur ce que personne ne vous avait jamais dit sur @extend.

Par

Sass nous offre énormément d’outils pour écrire un CSS cohérent et solide. L’un des plus puissants est @extend. La plupart des utilisateurs de Sass connaissent son utilisation, mais il reste quelques parties insuffisamment connues.

Commençons d’abord par les bases.

Les bases [email protected]

La directive @extend de Sass permet à un sélecteur d’hériter des styles d’un autre. C’est particulièrement utile dans une architecture basée sur les composants, en nous permettant de faire des variations à partir d’un composant.

Voici un cas très basique d’extension d’un sélecteur :

.message {
      padding: .5em;
}

.message-error {
      @extend .message;
}

Le résultat en CSS :

.message, .message-error { 
      padding: .5em;
}

Facile, non ? Bien sûr l’extension n’est pas limitée aux classes, vous pouvez étendre à peu près tous les sélecteurs (p.ex. a), les IDs (p.ex. #id) etc. et bien sûr les placeholders qui fonctionnent explicitement ainsi.

Extension d’un sélecteur complexe

La plupart du temps, on étend des sélecteurs simples (le plus souvent des classes), mais rien ne vous empêche d’étendre un sélecteur plus complexe, comme .class element:pseudo ou même :

.message + .message {
      margin-bottom: .5em;
}

.message-error {
      @extend .message;
}

qui sera rendu ainsi :

.message + .message, 
.message-error + .message-error, 
.message + .message-error, 
.message-error + .message { 
      margin-bottom: .5em;
}

Le résultat est maintenant un peu plus complexe, mais si vous comprenez @extend, il ne devrait pas trop vous surprendre.

Extensions multiples

Vous savez certainement que vous pouvez étendre des sélecteurs multiples dans la même règle, mais saviez-vous que vous pouviez le faire avec une directive @extend unique ?

.message {
      padding: .5em;
}

.important {
      font-weight: bold;
}

.message-error {
      @extend .message, .important;
}

La compilation en CSS donne :

.message, .message-error {
      padding: .5em;
}

.important, .message-error {
      font-weight: bold;
}

Si vous me posez la question, je vous dirais que cette méthode n’est pas ma préférée. Le code est certes plus concis, mais je le trouve plus difficile à lire. L’utilisation de directives distinctes pour chaque sélecteur étendu permet de mieux distinguer les sélecteurs et la façon dont ils sont étendus.

Vous pourriez certes réécrire la syntaxe ainsi :

.message-error {
      @extend
        .message,
        .important;
}

mais est-ce vraiment plus lisible ?

Pour ma part, je continuerai à écrire @extend ligne par ligne, le résultat sera identique, mais chacun peut faire comme il veut.

Enchaîner les extensions

Nous venons de voir qu’un sélecteur peut être étendu depuis plusieurs sources. Mais vous pouvez également enchaîner vos directives :

.message {
      padding: .5em;
}

.message-important {
      @extend .message;
      font-weight: bold;
}

.message-error {
      @extend .message-important;
}

La compilation en CSS donne :

.message, .message-important, .message-error {
      padding: .5em;
}

.message-important, message-error {
      font-weight: bold;
}

L’exemple ci-dessus est valide, toutefois je vous conseille de ne pas procéder ainsi pour éviter quelques effets indésirables que nous allons examiner maintenant.

Extension massive

Certains considèrent que le CSS produit par la compilation est horrible, et parfois ils peuvent avoir raison. La directive @extend est si puissante qu’elle peut aboutir à des extensions démesurées. C’est pourquoi il faut toujours être attentif lorsqu’on étend un sélecteur car il étendra toutes les occurrences du sélecteur - ce qui peut rapidement compliquer les choses.

.important {
      font-weight: bold;
}

.sidebar .important {
      color: red;
}

.message {
      @extend .important;
}

La compilation en CSS donne :

.important, .message {
      font-weight: bold;
}

.sidebar .important, .sidebar .message {
      color: red;
}

Comme vous le voyez, non seulement .message a hérité de .important, mais il a aussi hérité des instances où .important est un sélecteur descendant. Si c’est ce que vous aviez souhaité c’est parfait, mais il arrive que ce soit un effet non désiré, il convient donc d’être prudent avec la directive @extend, soit en s’assurant que le sélecteur à partir duquel vous faites l’extension n’existe qu’à un endroit dans votre CSS, soit en étendant un placeholder - qui est fait pour cela.

Dans tous les cas, rappelez-vous que Sass ne crée pas de mauvais code, ce sont les mauvais développeurs qui le font.

Préserver l’ordre des sources

Une caractéristique peu connue de @extend dans Sass est la façon dont elle traite l’ordre des sources. Prenons le code suivant :

.half-red {
      color: rgba(red, .5);
}

.message-error {
      color: red;
      @extend .half-red;
}

Mis à part le côté non-sémantique du nommage des classes, le code est correct et vous vous attendez à un résultat du genre :

/* NB: ceci n’est pas ce que vous obtiendrez */
.message-error {
      color: red;
      color: rgba(255, 0, 0, 0.5);
}

Ce serait une façon simple de permettre une dégradation élégante pour les navigateurs anciens qui ne supportent pas rgba(). Pourtant, ce n’est pas le résultat que nous obtiendrons, au contraire nous aurons :

.half-red, .message-error {
      color: rgba(255, 0, 0, 0.5);
}

.message-error {
      color: red;
}

Mais pourquoi donc ? me direz-vous. Eh bien parce que la directive @extend fonctionne à l’envers. La documentation Sass indique que :

@extend fonctionne en insérant le "sélecteur étendant" partout où le "sélecteur étendu" apparaît dans la feuille de style.

Dans notre exemple, le "sélecteur étendant" est .message-error et le sélecteur étendu est .half-red.

Extensions optionnelles

Selon les applications sur lesquelles vous travaillez, vous êtes peut-être amenés à utiliser des frameworks tiers ou des widgets. Si certains sont écrits en Sass, rien ne vous empêche d’étendre des sélecteurs de ces fichiers.

Malheureusement, si le sélecteur étendu n’existe pas, Sass renverra un message d’erreur et la compilation échouera :

".message-error" “ failed to @extend “.important”. The selector “.important” was not found.
Use [email protected] .important !optional” if the extend should be able to fail.

Comme vous le voyez, vous pouvez passer un flag !optional à vos imports pour éviter qu’ils n’échouent si le sélecteur étendu n’est pas trouvé. Ça peut être utile en cas de conflit entre les sélecteurs étendu et étendant :

a.important {
      font-weight: bold;
}

p.message-error {
      @extend .important;
}

De fait, cela enverra un message d’erreur car les deux sélecteurs sont spécifiés et il est donc impossible de les unifier :

No selectors matching “.important” could be unified with “p.message-error”.

L’ajout de !optional à votre @extend résoudra ce problème.

Extensions et media queries

L’un des principaux problèmes avec @extend est son incompatibilité avec la directive @media : impossible d’étendre à l’intérieur de @media un sélecteur défini à l’extérieur. Malheureusement, Sass ne permet pas les extensions cross-media.

.important {
      font-weight: bold;
}

@media (max-width: 767px) {
      .message-error {
        @extend .important;
      }
}

Vous obtiendrez le message d’erreur suivant :

You may not @extend an outer selector from within @media. You may only @extend selectors within the same directive.

Cela est dû au fait que @extend fonctionne en déplaçant les sélecteurs, pas les règles CSS, comme nous l’avons vu tout à l’heure au sujet de l’ordre des sources. Si Sass le permettait, alors l’extension d’un sélecteur situé dans un autre media query donnerait quelque chose comme cela :

.règle, @media(max-width: 767px) { .autre-règle }, .encore-une-autre-règle {/* ... */}

...ce qui n’est clairement pas du CSS valide.

Ceci étant, les développeurs de Sass sont tout à fait conscients du problème, comme le montre le nombre d’occurrences sur leur repo : #501, #640, #915, #1050, #1083.

Il est donc très probable qu’une solution soit proposée bientôt. Si l’on en croit ce commentaire de Nex3 (un des principaux développeurs de Sass), ce serait une mixin interpolation.

Bonnes pratiques

En résumé, voici ce que j’appellerais les bonnes pratiques [email protected]

  • Vérifiez que le sélecteur étendu est présent, une seule fois, dans la feuille de style.
  • Évitez d’étendre depuis des sélecteurs imbriqués.
  • Évitez d’enchaîner les directives @extend.
  • N’essayez pas d’étendre à l’intérieur d’un media query, ça ne marchera pas.

Intéressé par Sass ? Retrouvez une liste des meilleurs articles et ressources du web.

Tous les articles parus sur Sass dans La Cascade.


Ressources complémentaires en anglais

Ressources complémentaires en français


original paru le dans Sitepoint. Eh oui, Hugo fait partie de ces français talentueux qui écrivent directement en anglais pour s’adresser au monde entier et n’ont pas le temps de traduire pour le public francophone.

Sur l’auteur : est un intégrateur web de Grenoble, passionné par HTML5 et CSS3 et spécialiste de Sass. Vous pouvez lire ses articles ou contributions dans Sitepoint, TheSassWay, Codrops, CSS-tricks, et DavidWalshBlog.
Il est l’auteur de BrowserHacks, Sassylists, et bien d’autres choses Sassy.
On peut le suivre sur Twitter ou visiter son site.

Traduit avec l’aimable permission de Sitepoint et de l’auteur.
Copyright Sitepoint © 2014.