La Cascade

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

Comment fonctionne CSS display: contents

par Ire Aderinokun, 20 octobre 2018, css, semantique, article original paru le 27 mars 2017 dans Bits of Code

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.

2.5. Box Generation: the none and contents keywords

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 :

la boîte inner est affichée à l'intérieur de la boîte outer

Mais si nous ajoutons display: contents à l'élément .outer , voici ce que nous obtenons :

la boîte outer a disparu, il ne reste que son contenu : I'm some content

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 :

affichage du texte suivant : before i'm some content after

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; }
}

Autres ressources externes

Articles de Ire Aderinokun traduits dans La Cascade

Voir la page de Ire Aderinokun et la liste de ses articles dans La Cascade.
Article original paru le 27 mars 2017 dans Bits of Code
Traduit avec l'aimable autorisation de Bits of Code et de Ire Aderinokun.
Copyright Bits of Code © 2017