La Cascade

Un peu de tout sur CSS, HTML, SVG et JS,traduit du web anglophone
Rechercher

Sass, tout sur @extend

par Kitty Giraudel, 28 février 2017, css, sass, article original paru le 5 février 2014 dans Sitepoint

Kitty 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.


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 d’@extend

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: 0.5em;
}

.message-error {
  @extend .message;
}

qui est compilé en :

.message,
.message-error {
  padding: 0.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: 0.5em;
}

.message-error {
  @extend .message;
}

qui sera rendu ainsi :

.message + .message,
.message-error + .message-error,
.message + .message-error,
.message-error + .message {
  margin-bottom: 0.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: 0.5em;
}

.important {
  font-weight: bold;
}

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

La compilation en CSS donne :

.message,
.message-error {
  padding: 0.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;
}

nmais 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: 0.5em;
}

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

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

La compilation en CSS donne :

.message,
.message-important,
.message-error {
  padding: 0.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, 0.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 :

/* Ceci ne sera pas le résultat de la compilation ! */
.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 “@extend .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”.
Use “@extend .important !optional” if the extend should be able to fail.

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 d’@extend.

  • 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.

Autres ressources externes

Articles de Kitty Giraudel traduits dans La Cascade

Voir la page de Kitty Giraudel et la liste de ses articles dans La Cascade.
Article original paru le 5 février 2014 dans Sitepoint
Traduit avec l'aimable autorisation de Sitepoint et de Kitty Giraudel.
Copyright Sitepoint © 2014