La gestion des espaces avec flexbox n’est pas aussi simple qu’elle en a l’air. Voici une simple astuce que j’ai beaucoup utilisée ces derniers temps.
Le problème
Voici notre HTML pour cette démo :
<article>
<h1>Hello World</h1>
<ul>
<li>HTML</li>
<li>CSS</li>
<li>JavaScript</li>
<li>Front-end dev</li>
<li>Web</li>
</ul>
<p>Lorem ipsum...</p>
</article>
C’est un article avec une liste de tags. Avec un peu de CSS basique, voici à quoi il ressemble.
Nous souhaitons que la liste des tags soit un conteneur flex, avec la possibilité d’un retour à la ligne. Allons-y !
ul {
display: flex;
flex-wrap: wrap;
}
li {
margin-right: 2em;
}
Comme vous pouvez le voir, j’ai aussi ajouté de l’espace après chaque élément <li>
, 2em
étant égal à 32px
pour un texte basique (avec l’avantage de s’adapter aux préférences de l’utilisateur et à la taille de font de l’élément lui-même).
Et voici le résultat :
Cela peut sembler assez bien, mais le diable est dans les détails.
Regardez le coin inférieur droit de l’article : je l’ai rendu redimensionnable pour que vous puissez simuler un redimensionnement du navigateur.
Il y a deux problèmes principaux. Pouvez-vous les repérer ?
Le problème horizontal
Le premier problème est qu’à cause de sa marge à droite, le dernier élément passe à la ligne trop tôt.
Remarquez-vous comme le dernier élément aurait suffisamment d’espace pour aller plus loin à droite avant de revenir à la ligne ? Comparez cet espace à l’espace à gauche du premier élement.
Il serait possible de corriger ce problème en excluant le dernier élément de cette règle :
li:not(:last-child) {
margin-right: 2em;
}
Mais le problème est le même : tous les autres élément reviennent à la ligne trop tôt.
Désormais, le dernier élement peut aller plus loin vers la droite, mais les autres provoquent un retour à la ligne prématuré.
Bon, si margin-right
ne convient pas, que dire de margin-left
? Essayons de l’utiliser sur chaque élément, à l’exception du premier, qui ne devrait pas être précédé par un espace.
li:not(:first-child) {
margin-left: 2em;
}
Est-ce mieux désormais ? Prenez un moment pour essayer de deviner quel pourrait être le problème.
Puisqu’il n’y a plus de marge à droite, le retour à la ligne a lieu exactement quand il le faut. Mais maintenant, notre problème est ailleurs :
Nous ne sommes pas vraiment en position de nous plaindre. Nous avons demandé à CSS que tous les éléments sauf le premier aient une marge à gauche, et c’est ce qui se passe.
Comme il serait agréable de pouvoir exclure les éléments étant les premiers de leur ligne ! Mais il n’y a pas de sélecteur magique comme celui-ci :
li:not(:first-flex-row-item) {
/* N'existe pas */
}
Un tel sélecteur hypothétique pourrait causer une dépendance circulaire. Par exemple, je pourrais dire que le premier élément d’une ligne a une petite taille de font. Cela pourrait permettre à l’élément de retourner sur la ligne précédente (car il prendrait moins de place). Il ne serait donc plus ciblé par le sélecteur, reprendrait sa taille originale, reviendrait sur la seconde ligne et... 🤯
Les dépendances circulaires sont une des raisons principales pour lesquelles nous n’avons pas encore accès aux container queries. Mais c’est une autre histoire.
L’astuce de la marge négative
Voici donc la solution : d’abord, tous les éléments reçoivent un margin-left
.
li {
margin-left: 2em;
}
Nous devons maintenant nous débarrasser de l’espace à gauche du premier élément, et nous pouvons faire cela à l’aide de marges négatives.
Les marges négatives ne sont pas considérées comme faisant partie des bonnes pratiques, et je pense qu’il faut les éviter autant que possible, car elles peuvent rendre la logique de votre code plus difficile à comprendre.
Ceci étant dit, elles sont autorisées par le w3c et offrent un très bon support navigateur.
Et dans notre cas, elles nous sauvent la mise :
ul {
display: flex;
flex-wrap: wrap;
margin-left: -2em;
}
li {
margin-left: 2em;
}
Et l’axe vertical ?
Devinez quoi, c’est la même chose !
Il est impossible de cibler tous les éléments en excluant ceux qui sont sur la première ligne.
Il faut donc donner à tout le monde une marge supérieure.
li {
margin-left: 2em;
margin-top: 1em;
}
À cause de cet ajout, la liste apparaît plus bas que ce que nous souhaitons.
Il serait possible de retirer la propriété margin-bottom: 1em
du titre pour compenser.
Mais j’essaie de toujours garder mes éléments indépendants du contexte. La liste pourrait apparaître sous un autre élément quelque part ailleurs. Ou le titre pourrait être suivi par autre chose que cette liste.
Nous devons donc utiliser la même astuce et appliquer une marge négative à notre liste :
ul {
display: flex;
flex-wrap: wrap;
margin-left: -2em;
margin-top: -1em;
}
Et voici notre version finale. Elle fonctionne sur tous les navigateurs qui supportent correctement flexbox, y compris Internet Explorer 11.
Et la propriété gap
?
Les articles tels que celui-ci deviendront obsolète lorsque la propriété CSS gap
sera largement supportée.
Mais ce n’est pas encore le cas. À l’heure où j’écris ces lignes, le support navigateur est de 70%. Pas super, comparé au support à 99% de flexblox lui-même – Safari serait-il devenu le nouvel Internet Explorer ?
Tous les autres navigateurs modernes devraient vous montrer le même résultat avec le code suivant, sans astuce !
ul {
display: flex;
flex-wrap: wrap;
gap: 1em 2em; /* row-gap + column-gap */
}
/* Plus de style sur les items eux-mêmes */
Le point embêtant est qu’il n’est pas possible de détecter le support de cette propriété. Prenez le code suivant :
@supports (gap: 1em 2em) {
/* Désactiver les astuces et utiliser la solution propre */
}
La syntaxe @supports
permet d’appliquer des règles uniquement si le navigateur comprend ce qui se trouve entre les parenthèses.
Le problème est que gap
est également une propriété utilisée pour les grilles CSS, avec un bien meilleur support de 92%. Mais cela ne signifie pas que la propriété fonctionnera pour flexbox.
Le problème est étudié par le CSS Working Group.
En attendant, vive les marges négatives.
Et maintenant, avec des variables ✨
Nous pouvons améliorer notre code et le rendre plus générique si besoin. J’aime séparer mon CSS produisant des éléments BEM sémantiques et mes classes utilitaires. Je vais donc créer une classe utilitaire u-flex
.
Je ne suis pas un grand fan du fait d’avoir des classes orientées style dans mon HTML, et je pourrais donc utiliser une mixin SASS pour parvenir au même résultat, mais vous saisissez l’idée.
Utilisons les variables CSS, qui sont très bien supportées (95%). En CSS, il est possible d’obtenir l’opposé d’une valeur en la multipliant par -1
. Voici un exemple :
div {
--size: 2em;
width: calc(-1 * var(--size)); /* -2em */
}
Voici donc notre classe utilitaire :
.u-flex {
display: flex;
flex-wrap: wrap;
margin-top: calc(-1 * var(--row-gap));
margin-left: calc(-1 * var(--column-gap));
}
.u-flex > * {
margin-top: var(--row-gap);
margin-left: var(--column-gap);
}
J’aime utiliser le sélecteur d’enfant direct (> *
) avec flexbox et grid. Il s’accorde très bien avec la relation parent/enfants de ces fonctionnalités et fonctionnera à tous les coups.
Et voici comment je l’utiliserai :
<ul class="u-flex">
<li>HTML</li>
<li>CSS</li>
<li>JavaScript</li>
<li>Front-end dev</li>
<li>Web</li>
</ul>
ul {
--row-gap: 1em;
--column-gap: 2em;
}
Le pouvoir des variables CSS nous permet de définir des espaces différents pour chaque élément ciblé. Nous pourrions même définir des valeurs par défaut pour l’ensemble du document :
:root {
--row-gap: 1em;
--column-gap: 2em;
}
Ainsi, nous n’avons besoin de changer les variables localement que si nous souhaitons une valeur différente.
var(--row-gap, 1em);
Mauvaises pratiques
Nous connaissons toutes et tous un tas de mauvaises pratiques : !important
est une autre qui me vient à l’esprit. Mais comme les marges négatives, même !important
a des cas d’usages légitimes.
Cet astuce de la marge négative est un rappel pour moi : les choses que l’on apprend à éviter pourraient nous être bien utiles un jour ou l’autre. Tout dépend du contexte.
Cet article a été initialement publié sur dev.to
✍️ Aucun commentaire pour le moment