Contenu principal

Aligner des éléments avec CSS Grid et display: contents

lundi 28 août 2023

La semaine derniÚre, je me suis retrouvé face à un dilemme en CSS : comment faire pour aligner les images des articles alors que la taille de mes textes sont dynamiques ?

Screenshot d'une interface web qui affiche 3 articles distincts constitués d'un titre, d'une image et d'un texte
Code d'exemple

Comme vous pouvez le voir, si on utilise une approche classique, ce n’est pas trĂšs esthĂ©tique : les images sont dĂ©calĂ©es, on a l’Ɠil qui accroche sans que ce soit rĂ©ellement l’intention.

Dans cet article, je vais vous prĂ©senter en quelques mots les diffĂ©rentes approches auxquelles j’ai pensĂ© avant de vous parler de celle qui aujourd’hui me satisfait le mieux. Par ici, si vous voulez directement la solution.

Avant le style, le HTML

Avant de commencer Ă  penser CSS, il faut dĂ©jĂ  qu’on ait en tĂȘte le HTML qui permet d’afficher ce contenu. Notamment le but est d’ĂȘtre le plus sĂ©mantique possible. Par exemple ici avec l’utilisation de la balise article, en prenant bien soin de mettre le bon niveau de heading, etc.

<div class="article-list">
	<article class="article">
		<h2 class="article__title">Pawsitively Mysterious Purring</h2>
		<!--
            N'oubliez pas d'optimiser vos images en vous referrant à mon super article 😁
            https://www.julienpradet.fr/tutoriels/optimiser-le-chargement-des-images/
        -->
		<img
			alt="A cute random kitten"
			src="https://placekitten.com/g/300/200"
			width="300"
			height="200"
		/>
		<p>
			Cats are the only animals that can purr while both inhaling and exhaling,
            and the exact reason behind this unique behavior remains a scientific puzzle.
		</p>
	</article>

	<article class="article">
		<!-- Idem -->
	</article>
</div>

Les approches abandonnées

PremiÚre option : tronquer les textes

Une solution, certainement la plus simple techniquement est de tronquer les textes. Ainsi, on s’assure que rien ne dĂ©passe jamais et que tout est parfaitement alignĂ©.

MĂȘme screenshot qu'en dĂ©but d'article mais avec des `...` Ă  la fin du premier titre pour s'assurer qu'il est bien sur une seule ligne
.article-list {
	display: grid;
	grid-template-columns: repeat(3, 18rem);
	gap: 0 1rem;
	width: fit-content;
	margin: 0 auto;
}

.article__title {
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

💡 Petite astuce technique : pour que text-overflow: ellipsis fonctionne, il faut que l’élĂ©ment ait une taille fixe. Cela peut ĂȘtre un % ou comme ce que j’ai fait ici une taille fixe (18rem). Mais des unitĂ©s telles que 1fr ou auto ne fonctionneraient pas parce que le navigateur adapterait la largeur des articles plutĂŽt que de bloquer la largeur du titre.

Le problĂšme de cette approche, c’est qu’on va, par essence, dĂ©couper du texte. Et cela peut amener Ă  des situations
 cocasses.

Screenshot d'une prĂ©visualisation d'article de journal qui est titrĂ© 'PĂ©nurie d'eau : la Cour des comptes invite l’État Ă  frapper les gros cons...' parce que le dernier mot est tronquĂ©

Plus d’exemples et explications dans le Toot de Natouille “#UXWriting du jour : tronquer les textes ? Mauvaise idĂ©e.” sur Mastodon.

Dans mon cas, ça avait l’inconvĂ©nient supplĂ©mentaire que le titre Ă©tait utilisĂ© pour des noms d’artistes. Si j’écris “Anne-Sophie B
”, ça ne sert pas Ă  grand chose. Il y a certainement plein d’artistes dans le monde qui ont leur nom de famille qui commence par “B”. Et au delĂ  de l’aspect utile, je trouve que c’est tout simplement une marque de respect que de ne pas Ă©corcher les noms.

DeuxiÚme option : réserver suffisamment de place

Au maximum, mes titres peuvent ĂȘtre sur 3 lignes. Qu’à cela ne tienne, je vais simplement rĂ©server suffisamment de place pour afficher 3 lignes puis aligner sur cette hauteur le texte en bas.

.article__title {
	/* J'aligne par le bas grĂące Ă  flexbox */
	display: flex;
	align-items: end;
	/* 3 lines * font-size * line-height */
	min-height: calc(3 * 1em * 1.3);
}

A nouveau plusieurs inconvénients :

  • Aujourd’hui je sais que je ne dĂ©passerai jamais les 3 lignes. Que se passera-t-il si quelqu’un·e ajoute un nouvel article qui a un titre sur 4 lignes ? Que se passe-t-il pour les traductions dans les autres langues (exemple au hasard : l’allemand qui a tendance Ă  ĂȘtre bien plus long que l’anglais) ?

    Pour cette raison je vous conseille fortement de toujours utiliser min-height plutÎt que height. En effet, en passant par min-height, vous vous assurez que votre contenu reste lisible. Mais àa ne vous prémunira pas du décalage.

  • Que se passe-t-il si la sĂ©lection d’article du moment n’a que des titres courts ? On aura un trop grand espace blanc rĂ©servĂ© au dessus des articles. Ce n’est pas idĂ©al non plus.

TroisÚme option : aligner par le bas

Dans certains cas, aligner par le bas peut ĂȘtre une option :

.article-list {
	display: grid;
	grid-template-columns: repeat(3, 18rem);
	/* La totalité d'un article est aligné par le bas grùce à align-items */
	align-items: end;
	gap: 0 1rem;
	width: fit-content;
	margin: 0 auto;
}

Mais dans notre cas, ce n’est pas possible parce que non seulement la taille des titres est dynamique, mais la taille des descriptions sous l’image l’est aussi. Ainsi, mĂȘme si la derniĂšre ligne des descriptions est alignĂ©e, l’image ne l’est pas.

MĂȘme screenshot qu'en dĂ©but d'article mais la derniĂšre ligne des descriptions sont cette fois-ci bien alignĂ©es. Les images et les titres ne le sont toujours pas.

Ce n’est pas une solution qui nous convient, mais bien des situations, cette option m’a sauvĂ© lorsque les Ă©lĂ©ments du bas Ă©taient de taille fixe.

CSS Gird & display:contents Ă  la rescousse

Venons-en donc Ă  la solution que j’ai finalement adoptĂ©e.

Ma premiÚre idée était de considérer que, si je veux aligner convenablement les éléments, je vais devoir passer par une grille qui va sous découper mes articles en plusieurs parties.

Schema qui montre un tableau de 3 colonnes et 3 lignes. Une colonne par article. 1Ăšre ligne pour le titre, 2Ăšme pour l'image, 3Ăšme pour la description.

En rĂ©flĂ©chissant de la sorte, ça me permet de forcer l’alignement des lignes de mon tableau. De plus, il est alors possible d’avoir des rĂšgles d’alignement diffĂ©rentes en fonction de la ligne traitĂ©e (titre = alignĂ© en bas, description = alignĂ© en haut).

Cependant, par dĂ©faut, CSS Grid ne permet de positionner que ses enfants directs : il serait donc nĂ©cessaire d’aplatir la structure de notre HTML en enlevant la balise article pour pouvoir les placer individuellement.

<div class="article-list">
	<h2 class="article__title">...</h2>
	<img ... />
	<p>...</p>
	<h2 class="article__title">...</h2>
	<img ... />
	<p>...</p>
	<h2 class="article__title">...</h2>
	<img ... />
	<p>...</p>
</div>

Et le CSS devrait ressembler à quelque chose de ce style :

.article-list {
	display: grid;
	/* On indique au navigateur de remplir les columns en premier
     * afin que le titre, l'image et la description soient sur une
     * mĂȘme colonne plutĂŽt que sur une mĂȘme ligne */
	grid-auto-flow: column;
	/* On indique qu'il peut y avoir 3 lignes, pour qu'au 4Úme élément
     * on passe sur une nouvelle colonne */
	grid-template-rows: repeat(3, auto);
	grid-auto-columns: 18rem;
	gap: 0 1rem;

	margin: 0 auto;
	/* Les lignes suivantes ne sont pas en lien direct avec ce tuto mais 
     * permettent de centrer les articles horizontalement quand il y a assez
     * de place et de les aligner Ă  gauche quand il n'y en a plus assez.
     * (Si vous ne voulez pas de scroll mais réduire le
     * nombre de colonnes à la place, privilégiez des media queries) */
	max-width: fit-content;
	overflow: auto;
}

.article__title {
	/* On s'assure que le titre est bien aligné en bas */
	align-self: end;
}

⚠ Ca marche, mais on a vraiment pas envie d’enlever la balise article. Cela nuirai notamment au SEO en dĂ©tĂ©riorant la sĂ©mantique de votre page. Bof.

A la place, on va plutĂŽt expliquer au navigateur que certes il y a une balise article dans le HTML mais que pour gĂ©rer son CSS, il devra faire comme si elle n’existe pas (= lire le HTML comme l’exemple ci-dessus).

Et pour lui dire ça, on va utiliser la rÚgle display: contents.

.article {
	display: contents;
}

Tout le reste du CSS que j’avais Ă©crit juste au dessus peut rester identique ✹

Screenshot d'une interface web qui affiche 3 articles distincts constitués d'un titre, d'une image et d'un texte
Code d'exemple

On a donc tout le bĂ©nĂ©fice des CSS grids sans pour autant devoir nĂ©gocier avec le HTML. Et ça faisait longtemps que je n’avais pas eu besoin de triturer mon HTML pour des questions de style, alors j’avais envie de vous partager ça.

💡 Cette rĂšgle display: contents est trĂšs peu connue (en tout cas j’ai mis trĂšs longtemps avant de la dĂ©couvrir). Mais elle est peut-ĂȘtre utilisĂ©e Ă  plus d’endroits que vous ne l’imaginez. Notamment vous ĂȘtes peut-ĂȘtre familier avec la notion de <Fragment> en React. Et bien Svelte a choisi de l’implĂ©menter en insĂ©rant une balise div style="display: contents" Ă  la place de <svelte:fragment> dans le HTML gĂ©nĂ©rĂ©. C’est d’ailleurs comme ça que j’ai dĂ©couvert son existence.

Par ailleurs, d’autres articles avant moi parlent de cette mĂ©thode d’utilisation (More accessible markup with display: contents). Mais je n’en avais pas connaissance avant de faire des recherches pour cette article, alors le web mĂ©rite bien un nouvel article sur le sujet 😜

A propos de l’accessibilitĂ© de display: contents

Je vous ai dit plus haut que display: contents faisait disparaĂźtre la balise aux yeux du navigateur. Au niveau des specs, c’est vrai uniquement pour le style. Par contre, la balise doit rester disponible notamment pour l’accessibilitĂ© de la page.

Cela dit, les navigateurs ont longtemps eu des bugs Ă  ce sujet. C’est loin d’ĂȘtre mon expertise et Hidde en parle bien mieux que moi dans Accessibility concerns with current browser implementations.

Le TL;DR est que sur certains Ă©lĂ©ments, ça peut encore poser de rĂ©els problĂšmes aujourd’hui. Notamment, Ă©vitez Ă  tout prix d’utiliser display: contents sur des Ă©lĂ©ments interactifs (ex: button) ou des Ă©lĂ©ments qui ont des roles importants pour la bonne comprĂ©hension de votre page (ex: ul, li, tr, td, etc.). Veillez donc Ă  toujours bien tester avec des screen readers avant de vous lancer. Cette page tient Ă  jour les soucis d’accessibilitĂ© qu’il peut exister : https://adrianroselli.com/2022/07/its-mid-2022-and-browsers-mostly-safari-still-break-accessibility-via-display-properties.html

Bonus : subgrid

Pour ces mĂȘmes cas d’usage, une nouvelle fonctionnalitĂ© CSS est en train d’arriver : subgrid.

En quelques mots, c’est une valeur que vous pouvez utiliser dans la dĂ©finition de vos grid afin de prĂ©ciser Ă  une grille enfant de s’aligner avec les lignes et colonnes de la grille parente. Dans les faits, cela sera plus puissant que de juste appeler display: contents et c’est quelque chose qui mĂ©ritera sĂ»rement votre attention Ă  l’avenir.

Cependant Ă  ce jour c’est supportĂ© par seulement 18% des navigateurs. La bonne nouvelle c’est que c’est en train d’arriver dans Chrome 117 et donc on peut espĂ©rer une adoption large d’ici quelques mois quand ce sera essaimĂ© dans les diffĂ©rents navigateurs basĂ©s sur Chromium.

Bonus 2 : Avez-vous dĂ©jĂ  rĂȘvĂ© d’un ::after-outer ?

Les pseudo Ă©lĂ©ments :before et :after fonctionnent en ajoutant visuellement un balise span en dĂ©but ou en fin des enfants d’un Ă©lĂ©ment. Ainsi, le CSS suivant :

.element::before,
.element::after {
	content: '👋';
	display: block;
}

Nous donne l’équivalent de ce DOM :

<div class="parent">
	<div class="element">
		::before
		<!-- les enfants classiques -->
		::after
	</div>
</div>

Il peut arriver d’avoir besoin de les avoir autour du .element plutĂŽt qu’à l’intĂ©rieur. (Je n’ai pas de cas d’usages en tĂȘte mais on m’a posĂ© la question la semaine derniĂšre 😁)

<div class="parent">
	::before
	<div class="element">
		<!-- les enfants classiques -->
	</div>
	::after
</div>

L’astuce est alors d’ajouter une div autour de .element, de la mettre en display: contents et de dĂ©placer les :before et :after sur celle-ci:

<div class="parent">
	<div class="element-outer">
		::before
		<div class="element">
			<!-- les enfants classiques -->
		</div>
		::after
	</div>
</div>
.element-outer {
	display: contents;
}

.element-outer::before,
.element-outer::after {
	content: '👋';
	display: block;
}

Ainsi, vu que display: contents fait disparaĂźtre la balise aux yeux du navigateur, vous vous retrouvez dans la mĂȘme situation que si ::before, .element et ::after Ă©taient des enfants directs du .parent.

Voici un exemple d’utilisation avec lequel vous pouvez jouer : https://codepen.io/julienpradet/pen/XWoXKRO

Pour conclure

Bref, display: contents est une chouette solution mais pas non plus une solution miracle. Et subgrid rĂ©glera beaucoup plus de problĂšme pour nous. Ca reste la meilleure que j’ai trouvĂ© Ă  un instant T.

Est-ce que vous auriez fait diffĂ©remment ? Je serais trĂšs intĂ©ressĂ© de comparer les diffĂ©rentes approches possibles. N’hĂ©sitez pas Ă  me les envoyer sur les rĂ©seaux sociaux (Mastodon, LinkedIn ou Twitter).

En tout cas, si cet article vous a plu, sachez que j’essaye de publier un article par semaine autour du dĂ©veloppement web. Beaucoup d’entre eux sont orientĂ©s autour de la Web Performance qui se rapproche de mon activitĂ© de Freelance, mais vous n’ĂȘtes pas Ă  l’abri de sujets autour de l’intĂ©gration, du dĂ©veloppement front ou de l’organisation d’équipe. 😘


Si vous voulez suivre mes publications, il paraĂźt que j'ai un feed RSS, Mastodon et un Twitter.

Si vous pensez à d'autres méthodes que vous voudriez que je mette en place (pigeon voyageur, avion en papier, etc.), n'hésitez pas à me les proposer :)