Comment fonctionne CSS display: contents
Nous pouvons contrôler la façon dont la boîte et ses enfants sont dessinés sur la page grâce à la propriété CSS display
Comme je le rappelle souvent, chaque élément de l'arbre qui représente notre document (document tree) est une boîte rectangulaire. Grosso modo, cette boîte est faite de deux sections, l'une est la boîte elle-même — comprenant bordures, padding et marges — l'autre est le contenu.
Nous pouvons contrôler la façon dont cette boîte et ses enfants sont dessinés sur la page grâce à la propriété CSS display
. La boîte peut être placée parmi ses "soeurs" comme du texte, via inline
et nous pouvons même lui donner le comportement d'une table via table
.
Il n'y a que deux valeurs de la propriété display
qui permettent à un élément de générer (ou pas) une boîte. La valeur None
aura pour effet que ni la boîte ni son contenu ne seront affichés sur la page. La valeur contents
, elle, aura pour effet de bien afficher la boîte mais d'omettre totalement la boîte qui l'entoure.
Que se passe-t-il quand on utilise display: contents ?
Le plus simple pour le comprendre est d'imaginer qu'on a oublié les balises ouvrante et fermante de l'élément. Comme le dit la spécification :
Pour ce qui est de la génération d'une boîte et de son affichage, l'élément doit être traité comme s'il avait été remplacé par son contenu dans l'arbre du document.
Prenons le balisage suivant :
<div class="outer">
I’m some content
<div class="inner">I’m some inner content</div>
</div>
et les styles suivants :
.outer {
border: 2px solid lightcoral;
background-color: lightpink;
padding: 20px;
}
.inner {
background-color: #ffdb3a;
padding: 20px;
}
L'affichage attendu est :
Mais si nous ajoutons display: contents
à l'élément .outer
, voici ce que nous obtenons :
Visuellement, le résultat est exactement ce que nous obtiendrions en omettant les balises ouvrante et fermante de l'élément outer
.
I’m some content
<div class="inner">I’m some inner content</div>
Quid de... ?
Cette règle CSS, bien que simple en apparence, a quelques comportements spécifiques. Le point important à garder à l'esprit est que la règle display: contents
n'affecte que l'affichage de la boîte, il ne modifie en rien le balisage lui-même.
Quid des attributs de l'élément ?
Si, en pratique, l'élement est remplacé par son contenu, quelle est la conséquence pour ses attributs ? Puisque son remplacement est pour l'essentiel purement visuel, nous pouvons toujours sélectionner, cibler et interagir avec l'élément via ses attributs.
Par exemple nous pouvons cibler l'élément avec son ID pour le référencer avec aria-labelledby
:
<div id="label" style="display: contents;">Label here!</div>
<button aria-labelledby="label"><button>
Toutefois j'ai constaté que nous ne pouvons plus naviguer vers l'élément au moyen d'un identificateur de fragment (fragment identifier) :
<div id="target" style="display: contents;">Target Content</div>
<script>
window.location.hash = "target";
// => Nothing happens
</script>
Quid des events JS ?
Comme nous venons de le voir, nous pouvons cibler un élément sur lequel a été appliqué display: contents
. En fait, nous pouvons même cibler un élément ayant un style display: none
mais l'event ne sera jamais déclenché parce que nous ne pouvons pas interagir avec l'élément. Cependant avec display: contents
le contenu de l'élément étant toujours visible, nous pouvons interagir avec lui à travers son contenu.
Si par exemple nous appliquons un event listener pour un click sur l'élément, et que nous loggons la valeur de this
, nous obtenons toujours l'élément extérieur (outer) parce qu'il existe toujours dans le document.
<div class="outer">I’m some content</div>
<script>
document.querySelector(".outer").addEventListener("click", function(event) {
console.log(this);
// => <div class="outer"></div>
});
</script>
Quid des pseudo-éléments ?
Les pseudo-éléments d'un élément auquel est appliqué display: contents
sont considérés comme ses enfants et sont donc affichés normalement. ( NdT : pour une intro facile aux pseudo-éléments, voir ici un article sur les pseudo éléments css before et after.)
<style>
.outer { display: contents; }
.outer::before { content: "Before" }
.outer::after { content: "After" }
</style>
<div class="outer">I’m some content</div>
Le balisage ci-dessus donnera le résultat suivant :
Quid des formulaires, images et autres éléments remplacés ?
Les éléments remplacés et certains formulaires ont un comportement différent lorsqu'on leur applique content: display
.
Les éléments remplacés
Les éléments remplacés sont des éléments dont l'apparence et les "boîtes"sont contrôlées par une ressource externe – c'est le cas par exemple des images. Supprimer la boîte pour de tels éléments n'a pas réellement de sens car la notion de boîte n'est pas claire les concernant. Pour ces éléments, display: contents
fonctionne exactement comme display: none
. Ni la boîte ni son contenu ne seront affichés.
Les formulaires
Les éléments de formulaires semblent constitués d'une simple boîte, mais sous le capot ils sont en réalité constitués de nombreux éléments. Tout comme pour les éléments remplacés, supprimer la boîte n'a pas de sens car il n'y a pas une boîte. Du coup, pour les éléments de formulaires tels que <select>
, <input>
et <textarea>
, display: contents
fonctionne exactement comme display: none
.
(Voir la liste des éléments pour lesquels display: contents fonctionne différemment)
Quid des boutons et des liens ?
Les éléments <button>
et <a>
n'ont pas de comportement particulier avec display: contents
. Cependant il est utile de savoir comment cette règle les affecte car cela peut ne pas sembler évident.
Les boutons
Les boutons ne sont pas constitués de boîtes, par conséquent display: contents
ne fera que supprimer la boîte enveloppant le bouton, en ne laissant que le contenu du bouton. Si on l'utilise avec un formulaire, un clic sur ce bouton aura toujours pour effet de soumettre le formulaire et, comme nous l'avons vu, n'importe quel event listener sur le bouton fonctionnera normalement.
Les liens
La même chose s'applique aux liens, la boîte container est visuellement supprimée, ne laissant apparaître que le contenu du lien. Dans la mesure où les attributs ne sont généralement pas affectés par cette règle CSS, le lien fonctionnera toujours correctement et pourra être utilisé pour naviguer comme normalement.
Quelle est l'utilité de display: contents ?
Autrefois nous devions organiser notre HTML d'une façon qui fonctionne à la fois sémantiquement et pour notre style CSS. Cela a conduit parfois à créer trop d'éléments pour la sémantique, parfois trop peu pour le style, en particulier des éléments "frères". Ce dernier cas est particulièrement pertinent avec l'arrivée de CSS Grid Layout qui, au moins aujourd'hui, doit pouvoir travailler avec des éléments "frères".
Prenons par exemple ce layout :
Nous avons deux cartes placées côte à côte, chacune comprenant un titre, un paragraphe et un footer. Nous voulons que les sections aient la même hauteur pour toutes les cartes, indépendamment du contenu de chacune. Par exemple, la 2e carte a un titre de 3 lignes, la 1re carte a un titre de 1 ligne, mais les hauteurs de titres des deux cartes devraient être identiques.
Nous pourrions y arriver avec CSS Grid, mais pour cela il faudrait que tous les éléments dans une carte soient des "frères". Il nous faudrait organiser notre HTML comme ceci par exemple :
<div class="grid">
<h2>This is a heading</h2>
<p>...</p>
<p>Footer stuff</p>
<h2>This is a really really really super duper loooong heading</h2>
<p>...</p>
<p>Footer stuff</p>
</div>
et nous pourrions appliquer les styles suivants :
.grid {
display: grid;
grid-auto-flow: column;
grid-template-rows: auto 1fr auto;
grid-template-columns: repeat(2, 1fr);
grid-column-gap: 20px;
}
La structuration de ce document n'est certes pas incorrecte, mais il y aurait plus de sens à regrouper chaque carte dans un élément <article>
. C'est ici que display: contents
entre en scène, nous donnant le meilleur des deux mondes — en permettant un balisage sémantique tout en permettant d'organiser notre layout .
<div class="grid">
<article style="display: contents;">
<h2>This is a heading</h2>
<p>...</p>
<p>Footer stuff</p>
</article>
<article style="display: contents;">
<h2>This is a really really really super duper loooong heading</h2>
<p>...</p>
<p>Footer stuff</p>
</article>
</div>
Compatibilité
Comme toujours, la compatibilité peut être vérifiée sur CanIUse, elle est bonne aujourd'hui, à l'exception de IE.
Si vous souhaitez assurer une compatibilité maximale, vous pouvez considérer cette feature comme une amélioration progressive (progressive enhancement) et utiliser une solution de rechange (fallback) appropriée.
article {
display: grid;
grid-template-rows: 200px 1fr auto; /* e.g. Use a fixed height for the header */
}
@supports (display: contents) {
article { display: contents; }
}