La Cascade

Rechercher

Une table des matières parfaite avec HTML et CSS

par Nicholas C Zakas, 25 juillet 2022, css, html, accessibilite, article original paru le 25 mai 2022 dans CSS-Tricks

Créer une table des matières pour un document à imprimer s'avère légèrement compliqué, Nicholas C. Zakas propose ici une solution simple en purs HTML et CSS.


Au début de cette année, j'ai auto-publié un ebook intitulé Understanding JavaScript Promises (téléchargeable gratuitement). Je n'avais pas l'intention d'en faire un livre imprimé, mais pas mal de personnes m'ayant contacté pour me demander une version imprimée, j'ai décidé de l'auto-publier également. Je me disais qu'il ne serait pas trop compliqué d'utiliser HTML et CSS pour générer un PDF et l'envoyer à l'imprimeur. Ce que je n'avais pas encore réalisé, c'est que je n'avais pas de solution pour gérer une partie importante d'un livre imprimé : la table des matières.

La composition d'une table des matières

À la base, une table des matières est assez simple. Chaque ligne représente une partie d'un livre ou d'une page Web et indique où vous pouvez trouver ce contenu. En général, les lignes contiennent trois parties :

  1. Le titre du chapitre ou de la section
  2. Des amorces (c'est-à-dire des points, des tirets ou des lignes) qui relient visuellement le titre au numéro de page.
  3. Le numéro de page

On peut facilement générer une table des matières à l'aide d'outils de traitement de texte comme Microsoft Word ou Google Docs, mais comme mon contenu était en format Markdown, puis transformé en HTML, ce n'était pas une bonne option pour moi. Je voulais quelque chose d'automatisé qui fonctionne avec HTML pour générer la table des matières dans un format adapté à l'impression. Je voulais également que chaque ligne soit un lien afin de pouvoir l'utiliser dans les pages Web et les PDF pour naviguer dans le document. Je voulais aussi que le titre et le numéro de page soient précédés de points.

J'ai donc commencé à faire des recherches.

Je suis tombé sur deux excellents articles de blog sur la création d'une table des matières avec HTML et CSS. Le premier, intitulé "Build a Table of Contents from your HTML", est signé Julie Blanc. Julie a travaillé sur PagedJS, un polyfill pour les fonctionnalités de médias paginés manquantes dans les navigateurs Web qui formate correctement les documents pour l'impression. J'ai commencé par l'exemple de Julie, mais j'ai trouvé qu'il ne fonctionnait pas vraiment pour moi. J'ai ensuite trouvé le billet de Christoph Grabo intitulé "Responsive TOC leader lines with CSS", qui présentait le concept d'utilisation de CSS Grid (par opposition à l'approche basée sur les float de Julie) pour faciliter l'alignement. Une fois de plus, cependant, son approche ne correspondait pas tout à fait à mes besoins.

Mais après avoir lu ces deux articles, j'ai eu le sentiment d'avoir une compréhension suffisante des problèmes de mise en page pour me lancer dans mon propre projet. J'ai utilisé des éléments des deux articles du blog et j'ai ajouté quelques nouveaux concepts HTML et CSS à mon approche pour arriver à un résultat qui me convient.

Choix du balisage

Lorsque j'ai décidé du balisage correct d'une table des matières, j'ai surtout pensé à la sémantique correcte. Fondamentalement, une table des matières consiste à associer un titre (chapitre ou sous-section) à un numéro de page, presque comme une paire clé-valeur. Cela m'a conduit à deux options :

La première option consiste à utiliser un tableau (<table>) avec une colonne pour le titre et une colonne pour la page. Ensuite, il y a l'élément liste de définition (<dl>), souvent inutilisé et oublié. Il fait également office de carte clé-valeur. Ainsi, une fois encore, la relation entre le titre et le numéro de page serait évidente.

Ces deux options semblaient bonnes jusqu'à ce que je me rende compte qu'elles ne fonctionnent vraiment que pour les tables des matières à un seul niveau, c'est-à-dire uniquement si je voulais avoir une table des matières contenant uniquement les noms des chapitres. En revanche, si je voulais afficher les sous-sections dans la table des matières, je n'avais pas de bonnes options. Les éléments de tableau ne sont pas très adaptés aux données hiérarchiques et, bien que les listes de définitions puissent techniquement être imbriquées, la sémantique ne semblait pas correcte. Je suis donc retourné à la planche à dessin.

J'ai décidé de m'inspirer de l'approche de Julie et d'utiliser une liste ; cependant, j'ai opté pour une liste ordonnée (<ol>) au lieu d'une liste non ordonnée (<ul>). Je pense qu'une liste ordonnée est plus appropriée dans ce cas. Une table des matières représente une liste de chapitres et de sous-titres dans l'ordre dans lequel ils apparaissent dans le contenu. L'ordre a de l'importance et ne doit pas se perdre dans le balisage.

Malheureusement, l'utilisation d'une liste ordonnée implique la perte de la relation sémantique entre le titre et le numéro de page. L'étape suivante consistait donc à rétablir cette relation dans chaque élément de la liste. Le moyen le plus simple de résoudre ce problème est d'insérer le mot "page" avant le numéro de page. De cette façon, la relation entre le numéro et le texte est claire, même sans autre distinction visuelle.

Voici un squelette HTML simple qui a servi de base à mon balisage :

<ol class="toc-list">
  <li>
    <a href="#link_to_heading">
      <span class="title">Chapître ou titres de sous-sections</span>
      <span class="page">Page 1</span>
    </a>

    <ol>
      <!-- sous-sections -->
    </ol>
  </li>
</ol>

Application de styles à la table des matières

Une fois que j'ai établi le balisage que je prévoyais d'utiliser, l'étape suivante consistait à appliquer certains styles.

Tout d'abord, j'ai supprimé les numéros générés automatiquement. Vous pouvez choisir de conserver les numéros générés automatiquement dans votre propre projet si vous le souhaitez, mais il est fréquent que les livres aient des avant et après-mots non numérotés inclus dans la liste des chapitres, ce qui rend les numéros générés automatiquement incorrects.

Pour mon cas, je remplirais les numéros de chapitre manuellement, puis j'ajusterais la mise en page de façon à ce que la liste de premier niveau n'ait pas de remplissage (ce qui l'aligne sur les paragraphes) et que chaque liste intégrée soit indentée de deux espaces. J'ai choisi d'utiliser une valeur de remplissage de 2 ch parce que je n'étais pas encore tout à fait sûr de la police que j'allais utiliser. L'unité de longueur ch permet au remplissage d'être relatif à la largeur d'un caractère - quelle que soit la police utilisée - plutôt qu'à une taille absolue en pixels qui pourrait donner un aspect incohérent.

Voici le CSS que j'ai obtenu :

.toc-list,
.toc-list ol {
  list-style-type: none;
}

.toc-list {
  padding: 0;
}

.toc-list ol {
  padding-inline-start: 2ch;
}

Sara Soueidan m'a fait remarquer que les navigateurs WebKit suppriment la sémantique des listes lorsque list-style-type est none. J'ai donc dû ajouter role="list" dans le HTML pour la préserver :

<ol class="toc-list" role="list">
  <li>
    <a href="#link_to_heading">
      <span class="title">Chapître ou titres de sous-sections</span>
      <span class="page">Page 1</span>
    </a>

    <ol role="list">
      <!-- sous-sections -->
    </ol>
  </li>
</ol>
voir Table of Contents - Start de Nicholas C. Zakas dans CodePen

Stylisation du titre et du numéro de page

La liste étant stylisée à mon goût, il était temps de passer au style d'un élément individuel de la liste. Pour chaque élément de la table des matières, le titre et le numéro de page doivent être sur la même ligne, avec le titre à gauche et le numéro de page aligné à droite.

Vous vous dites peut-être : "Pas de problème, c'est à ça que sert le flexbox !" Vous n'avez pas tort ! Flexbox permet en effet d'obtenir un alignement correct du titre et de la page. Mais il y a quelques problèmes d'alignement délicats lorsque les leaders sont ajoutés, j'ai donc opté pour l'approche de Christoph qui utilise une grille, ce qui est un bonus car cela aide aussi avec les titres multilignes. Voici le CSS pour un article individuel :

.toc-list li > a {
  text-decoration: none;
  display: grid;
  grid-template-columns: auto max-content;
  align-items: end;
}

.toc-list li > a > .page {
  text-align: right;
}

La grille comporte deux colonnes, dont la première est automatiquement dimensionnée pour remplir toute la largeur du conteneur, moins la deuxième colonne, qui est dimensionnée au contenu maximal. Le numéro de page est aligné sur la droite, comme il est de coutume dans une table des matières.

La seule autre modification que j'ai apportée à ce stade a été de masquer le texte "Page". Ce texte est utile pour les lecteurs d'écran, mais inutile sur le plan visuel. J'ai donc utilisé une classe traditionnelle de type "visually-hidden" pour le masquer :

.visually-hidden {
  clip: rect(0 0 0 0);
  clip-path: inset(100%);
  height: 1px;
  overflow: hidden;
  position: absolute;
  width: 1px;
  white-space: nowrap;
}

Et, bien entendu, le HTML doit être mis à jour pour utiliser cette classe :

<ol class="toc-list" role="list">
  <li>
    <a href="#link_to_heading">
      <span class="title">Chapître ou titres de sous-sections</span>
      <span class="page"><span class="visually-hidden">Page</span> 1</span>
    </a>

    <ol role="list">
      <!-- sous-sections -->
    </ol>
  </li>
</ol>

Cette base étant posée, je suis passé à la question des lignes pointillées entre le titre et la page.

voir Table of Contents - Étape 2 de Nicholas C. Zakas dans CodePen

Création de lignes pointillées

Les lignes pointillées sont si courantes dans les médias imprimés que vous vous demandez peut-être pourquoi le CSS ne les prend pas déjà en charge. La réponse est : c'est le cas. Enfin, plus ou moins...

Il existe en fait une fonction leader() définie dans la spécification CSS Generated Content for Paged Media. Toutefois, comme c'est le cas pour la plupart des spécifications relatives aux médias paginés, cette fonction n'est implémentée dans aucun navigateur, ce qui l'exclut de toute option (du moins au moment où j'écris ces lignes). Elle n'est même pas répertoriée sur caniuse.com, sans doute parce que personne ne l'a implémentée et qu'il n'y a aucun plan ou signe indiquant qu'ils le feront.

Heureusement, Julie et Christoph ont déjà abordé ce problème dans leurs articles respectifs. Pour insérer les lignes pointillées, ils ont tous deux utilisé un pseudo-élément ::after dont la propriété de contenu est définie comme une très longue chaîne de points, comme ceci :

.toc-list li > a > .title {
  position: relative;
  overflow: hidden;
}

.toc-list li > a .title::after {
  position: absolute;
  padding-left: 0.25ch;
  content: ' . . . . . . . . . . . . . . . . . . . '
    '. . . . . . . . . . . . . . . . . . . . . . . '
    '. . . . . . . . . . . . . . . . . . . . . . . '
    '. . . . . . . . . . . . . . . . . . . . . . . '
    '. . . . . . . . . . . . . . . . . . . . . . . '
    '. . . . . . . . . . . . . . . . . . . . . . . '
    '. . . . . . . . . . . . . . . . . . . . . . . ';
  text-align: right;
}

Le pseudo-élément ::after est placé dans une position absolue pour le sortir du flux de la page et éviter qu'il ne revienne à la ligne. Le texte est aligné sur la droite parce que nous voulons que les derniers points de chaque ligne soient alignés sur le numéro en fin de ligne. (Nous reviendrons plus tard sur les complexités de cette question.) L'élément .title est configuré pour avoir une position relative afin que le pseudo-élément ::after ne sorte pas de sa boîte. Pendant ce temps, le débordement est masqué pour que tous ces points supplémentaires soient invisibles. Le résultat est une jolie table des matières avec des lignes pointillées.

Cependant, il y a encore autre chose à prendre en considération.

Sara m'a également fait remarquer que tous ces points sont considérés comme du texte par les lecteurs d'écran. Et du coup, qu'est-ce qu'on entend ? "Introduction point point point point..." jusqu'à ce que tous les points soient annoncés. C'est une expérience terrible pour les utilisateurs de lecteurs d'écran.

La solution consiste à insérer un élément supplémentaire dont l'aria-hidden est réglé sur true, puis à utiliser cet élément pour insérer les points. Ainsi, le HTML devient :

<ol class="toc-list" role="list">
  <li>
    <a href="#link_to_heading">
      <span class="title"
        >Chapître ou titres de sous-sections<span
          class="leaders"
          aria-hidden="true"
        ></span
      ></span>
      <span class="page"><span class="visually-hidden">Page</span> 1</span>
    </a>

    <ol role="list">
      <!-- sous-sections -->
    </ol>
  </li>
</ol>

Et le CSS devient :

.toc-list li > a > .title {
  position: relative;
  overflow: hidden;
}

.toc-list li > a .leaders::after {
  position: absolute;
  padding-left: 0.25ch;
  content: ' . . . . . . . . . . . . . . . . . . . '
    '. . . . . . . . . . . . . . . . . . . . . . . '
    '. . . . . . . . . . . . . . . . . . . . . . . '
    '. . . . . . . . . . . . . . . . . . . . . . . '
    '. . . . . . . . . . . . . . . . . . . . . . . '
    '. . . . . . . . . . . . . . . . . . . . . . . '
    '. . . . . . . . . . . . . . . . . . . . . . . ';
  text-align: right;
}

Désormais, les lecteurs d'écran ignoreront les points et épargneront aux utilisateurs la frustration d'entendre l'annonce de plusieurs points.

voir Table of Contents - Étape 3 de Nicholas C. Zakas dans CodePen

Touches finales

À ce stade, le composant table des matières est plutôt bien conçu, mais il pourrait bénéficier de quelques petites retouches. Pour commencer, la plupart des livres décalent visuellement les titres des chapitres de ceux des sous-sections. J'ai donc mis les éléments de premier niveau en gras et introduit une marge pour séparer les sous-sections des chapitres qui suivent :

.toc-list > li > a {
  font-weight: bold;
  margin-block-start: 1em;
}

Ensuite, je voulais nettoyer l'alignement des numéros de page. Tout se passait bien lorsque j'utilisais une police à largeur fixe, mais pour les polices à largeur variable, les points de repère peuvent finir par former un motif en zigzag lorsqu'ils s'adaptent à la largeur d'un numéro de page. Par exemple, les numéros de page comportant un 1 sont plus étroits que les autres, ce qui entraîne un désalignement des points d'amorce par rapport aux points des lignes précédentes ou suivantes.

Pour résoudre ce problème, j'ai réglé font-variant-numeric sur tabular-nums afin que tous les chiffres soient traités avec la même largeur. En fixant également la largeur minimale à 2ch, je me suis assuré que tous les chiffres à un ou deux chiffres étaient parfaitement alignés. (Si votre projet compte plus de 100 pages, il est préférable de fixer cette valeur à 3ch). Voici le CSS final pour le numéro de page :

.toc-list li > a > .page {
  min-width: 2ch;
  font-variant-numeric: tabular-nums;
  text-align: right;
}

Et voilà, la table des matières est complète !

voir Table of Contents - Final de Nicholas C. Zakas dans CodePen

Conclusion

La création d'une table des matières avec rien d'autre que du HTML et du CSS a été un défi plus important que prévu, mais je suis très satisfait du résultat. Non seulement cette approche est suffisamment souple pour s'adapter aux chapitres et aux sous-sections, mais elle permet de gérer les sous-sous-sections sans mettre à jour le CSS. L'approche globale fonctionne sur les pages Web où vous souhaitez créer des liens vers les différents emplacements du contenu, ainsi que sur les PDF où vous souhaitez que la table des matières renvoie à différentes pages. Et bien sûr, elle est également très belle à l'impression si vous souhaitez l'utiliser dans une brochure ou un livre.

Je tiens à remercier Julie Blanc et Christoph Grabo pour leurs excellents articles de blog sur la création d'une table des matières, car ils m'ont été d'une aide précieuse lorsque j'ai commencé. J'aimerais également remercier Sara Soueidan pour ses commentaires sur l'accessibilité pendant que je travaillais sur ce projet.

Voir la liste des articles de Nicholas C Zakas traduits dans La Cascade.
Article original paru le 25 mai 2022 dans CSS-Tricks
Traduit avec l'aimable autorisation de CSS-Tricks et de Nicholas C Zakas.
Copyright CSS-Tricks © 2022