Ein perfektes Inhaltsverzeichnis mit HTML + CSS

Avatar of Nicholas C. Zakas
Nicholas C. Zakas am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Anfang des Jahres habe ich ein E-Book mit dem Titel Understanding JavaScript Promises (kostenloser Download) selbst veröffentlicht. Obwohl ich keine Absicht hatte, daraus ein gedrucktes Buch zu machen, haben so viele Leute nach einer gedruckten Version gefragt, dass ich mich entschied, auch diese selbst zu veröffentlichen. Ich dachte, es wäre eine einfache Übung, mit HTML und CSS eine PDF zu erstellen und sie dann an den Drucker zu schicken. Was ich nicht erkannte, war, dass ich keine Antwort auf einen wichtigen Teil eines gedruckten Buches hatte: das Inhaltsverzeichnis.

Der Aufbau eines Inhaltsverzeichnisses

Im Kern ist ein Inhaltsverzeichnis ziemlich einfach. Jede Zeile repräsentiert einen Teil eines Buches oder einer Webseite und gibt an, wo Sie diesen Inhalt finden können. Typischerweise enthalten die Zeilen drei Teile:

  1. Der Titel des Kapitels oder Abschnitts
  2. Füllzeichen (d. h. diese Punkte, Striche oder Linien), die den Titel visuell mit der Seitenzahl verbinden
  3. Die Seitenzahl

Ein Inhaltsverzeichnis lässt sich leicht in Textverarbeitungsprogrammen wie Microsoft Word oder Google Docs erstellen, aber da meine Inhalte in Markdown und dann in HTML umgewandelt wurden, war das für mich keine gute Option. Ich wollte etwas Automatisiertes, das mit HTML funktioniert, um das Inhaltsverzeichnis in einem für den Druck geeigneten Format zu generieren. Außerdem wollte ich, dass jede Zeile ein Link ist, damit sie auf Webseiten und in PDFs zur Navigation im Dokument verwendet werden kann. Ich wollte auch Punktfüllzeichen zwischen Titel und Seitenzahl.

Und so begann ich zu recherchieren.

Ich stieß auf zwei ausgezeichnete Blogbeiträge über die Erstellung eines Inhaltsverzeichnisses mit HTML und CSS. Der erste war „Build a Table of Contents from your HTML“ von Julie Blanc. Julie arbeitete an PagedJS, einem Polyfill für fehlende Paged-Media-Funktionen in Webbrowsern, der Dokumente für den Druck richtig formatiert. Ich begann mit Julies Beispiel, fand aber, dass es für mich nicht ganz funktionierte. Als nächstes fand ich Christoph Grabos „Responsive TOC leader lines with CSS“, der das Konzept der Verwendung von CSS Grid (im Gegensatz zu Julies Float-basierendem Ansatz) einführte, um die Ausrichtung zu erleichtern. Aber auch hier war sein Ansatz nicht ganz richtig für meine Zwecke.

Nachdem ich diese beiden Beiträge gelesen hatte, hatte ich jedoch ein ausreichend gutes Verständnis der Layout-Probleme, um mein eigenes zu beginnen. Ich verwendete Teile aus beiden Blogbeiträgen und fügte einige neue HTML- und CSS-Konzepte hinzu, um ein Ergebnis zu erzielen, mit dem ich zufrieden bin.

Wahl des richtigen Markups

Bei der Entscheidung für das richtige Markup für ein Inhaltsverzeichnis dachte ich hauptsächlich an die richtige Semantik. Grundsätzlich geht es bei einem Inhaltsverzeichnis darum, dass ein Titel (Kapitel oder Unterabschnitt) mit einer Seitenzahl verbunden ist, fast wie ein Schlüssel-Wert-Paar. Das brachte mich zu zwei Optionen:

  • Eine Option ist die Verwendung einer Tabelle (<table>) mit einer Spalte für den Titel und einer Spalte für die Seite.
  • Dann gibt es noch das oft ungenutzte und vergessene Definition Listen (<dl>) Element. Es fungiert ebenfalls als Schlüssel-Wert-Map. So wäre die Beziehung zwischen Titel und Seitenzahl wieder klar.

Beides schien eine gute Option zu sein, bis ich erkannte, dass sie wirklich nur für einstufige Inhaltsverzeichnisse funktionieren, nämlich nur, wenn ich ein Inhaltsverzeichnis mit nur Kapitelnamen haben wollte. Wenn ich jedoch Unterabschnitte im Inhaltsverzeichnis anzeigen wollte, hatte ich keine guten Optionen. Tabellenelemente eignen sich nicht gut für hierarchische Daten, und obwohl Definitionslisten technisch verschachtelt werden können, schien die Semantik nicht korrekt. Also ging ich zurück an den Zeichentisch.

Ich beschloss, auf Julies Ansatz aufzubauen und eine Liste zu verwenden; ich entschied mich jedoch für eine geordnete Liste (<ol>) anstelle einer ungeordneten Liste (<ul>). Ich denke, eine geordnete Liste ist in diesem Fall angemessener. Ein Inhaltsverzeichnis repräsentiert eine Liste von Kapiteln und Unterüberschriften in der Reihenfolge, in der sie im Inhalt erscheinen. Die Reihenfolge ist wichtig und sollte im Markup nicht verloren gehen.

Leider geht bei der Verwendung einer geordneten Liste die semantische Beziehung zwischen Titel und Seitenzahl verloren, so dass mein nächster Schritt darin bestand, diese Beziehung innerhalb jedes Listenelements wiederherzustellen. Der einfachste Weg, dies zu lösen, ist, einfach das Wort „Seite“ vor die Seitenzahl zu setzen. So ist die Beziehung der Zahl zum Text klar, auch ohne andere visuelle Unterscheidungen.

Hier ist ein einfaches HTML-Skelett, das die Grundlage meines Markups bildete:

<ol class="toc-list">
  <li>
    <a href="#link_to_heading">
      <span class="title">Chapter or subsection title</span>
      <span class="page">Page 1</span>
    </a>

    <ol>
      <!-- subsection items -->
    </ol>
  </li>
</ol>

Anwenden von Stilen auf das Inhaltsverzeichnis

Nachdem ich das geplante Markup festgelegt hatte, war der nächste Schritt das Anwenden einiger Stile.

Zuerst habe ich die automatisch generierten Nummern entfernt. Sie können die automatisch generierten Nummern in Ihrem eigenen Projekt beibehalten, wenn Sie möchten, aber es ist üblich, dass Bücher unnummerierte Vorworte und Nachworte enthalten, die in die Kapitelübersicht aufgenommen werden, was die automatisch generierten Nummern falsch macht.

Für meinen Zweck würde ich die Kapitelnummern manuell eintragen und dann das Layout so anpassen, dass die oberste Listenebene keine Einrückung hat (wodurch sie mit Absätzen übereinstimmt) und jede eingebettete Liste um zwei Leerzeichen eingerückt wird. Ich habe mich für einen 2ch Padding-Wert entschieden, da ich mir noch nicht sicher war, welche Schriftart ich verwenden würde. Die ch Längeneinheit ermöglicht es, dass der Abstand relativ zur Breite eines Zeichens ist – unabhängig von der verwendeten Schriftart – anstatt eine absolute Pixelgröße, die inkonsistent aussehen könnte.

Hier ist das CSS, zu dem ich am Ende gekommen bin:

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

.toc-list {
  padding: 0;
}

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

Sara Soueidan wies mich darauf hin, dass WebKit-Browser Listen-Semantik entfernen, wenn list-style-type auf none gesetzt ist. Daher musste ich role="list" zum HTML hinzufügen, um sie zu erhalten.

<ol class="toc-list" role="list">
  <li>
    <a href="#link_to_heading">
      <span class="title">Chapter or subsection title</span>
      <span class="page">Page 1</span>
    </a>

    <ol role="list">
      <!-- subsection items -->
    </ol>
  </li>
</ol>

Formatierung von Titel und Seitenzahl

Nachdem die Liste nach meinen Wünschen formatiert war, ging es an die Formatierung eines einzelnen Listenelements. Bei jedem Eintrag im Inhaltsverzeichnis müssen Titel und Seitenzahl auf derselben Zeile stehen, wobei der Titel links und die Seitenzahl rechts ausgerichtet ist.

Sie denken vielleicht: „Kein Problem, dafür ist Flexbox da!“ Da liegen Sie nicht falsch! Flexbox kann die korrekte Ausrichtung von Titel und Seite tatsächlich erreichen. Aber es gibt einige knifflige Ausrichtungsprobleme, wenn die Füllzeichen hinzugefügt werden. Deshalb entschied ich mich stattdessen für Christophs Ansatz mit einem Grid, der nebenbei auch bei mehrzeiligen Titeln hilft. Hier ist das CSS für einen einzelnen Eintrag:

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

Das Grid hat zwei Spalten, von denen die erste auto-groß ist, um die gesamte Breite des Containers auszufüllen, abzüglich der zweiten Spalte, die auf max-content eingestellt ist. Die Seitenzahl ist rechtsbündig, wie es in einem Inhaltsverzeichnis üblich ist.

Die einzige weitere Änderung, die ich zu diesem Zeitpunkt vorgenommen habe, war das Ausblenden des Wortes „Seite“. Dies ist hilfreich für Screenreader, aber visuell unnötig, daher habe ich eine traditionelle visually-hidden Klasse verwendet, um es auszublenden.

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

Und natürlich muss das HTML aktualisiert werden, um diese Klasse zu verwenden:

<ol class="toc-list" role="list">
  <li>
    <a href="#link_to_heading">
      <span class="title">Chapter or subsection title</span>
      <span class="page"><span class="visually-hidden">Page</span> 1</span>
    </a>

    <ol role="list">
      <!-- subsection items -->
    </ol>
  </li>
</ol>

Mit dieser Grundlage ging es darum, die Füllzeichen zwischen Titel und Seite anzugehen.

Erstellen von Punktfüllzeichen

Füllzeichen sind in gedruckten Medien so üblich, dass Sie sich vielleicht fragen: Warum unterstützt CSS das nicht schon? Die Antwort lautet: Tut es. Nun, irgendwie.

Es gibt tatsächlich eine leader() Funktion in der CSS Generated Content for Paged Media specification. Wie bei vielen Paged-Media-Spezifikationen ist diese Funktion jedoch nicht in Browsern implementiert und daher keine Option (zumindest zum Zeitpunkt des Schreibens). Sie wird nicht einmal auf caniuse.com aufgeführt, vermutlich weil sie niemand implementiert hat und keine Pläne oder Signale dafür existieren.

Glücklicherweise haben sowohl Julie als auch Christoph dieses Problem in ihren jeweiligen Beiträgen bereits angesprochen. Um die Punktfüllzeichen einzufügen, verwendeten beide ein ::after Pseudoelement mit der content Eigenschaft auf eine sehr lange Zeichenkette von Punkten gesetzt, so:

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

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

Das ::after Pseudoelement wird absolut positioniert, um es aus dem Seitenfluss zu nehmen und ein Umbrechen auf andere Zeilen zu verhindern. Der Text ist rechtsbündig ausgerichtet, da wir möchten, dass die letzten Punkte jeder Zeile bündig mit der Zahl am Ende der Zeile sind. (Mehr zu den Komplexitäten davon später.) Das .title Element ist relativ positioniert, damit das ::after Pseudoelement nicht aus seinem Feld ausbricht. In der Zwischenzeit wird overflow ausgeblendet, sodass all diese zusätzlichen Punkte unsichtbar sind. Das Ergebnis ist ein schönes Inhaltsverzeichnis mit Punktfüllzeichen.

Es gibt jedoch noch etwas zu beachten.

Sara wies mich auch darauf hin, dass all diese Punkte für Screenreader als Text zählen. Was hört man also? „Einleitung Punkt Punkt Punkt Punkt…“ bis alle Punkte angesagt sind. Das ist eine schreckliche Erfahrung für Screenreader-Benutzer.

Die Lösung ist, ein zusätzliches Element mit aria-hidden auf true zu setzen und dann dieses Element zu verwenden, um die Punkte einzufügen. Der HTML-Code sieht dann so aus:

<ol class="toc-list" role="list">
  <li>
    <a href="#link_to_heading">
      <span class="title">Chapter or subsection title<span class="leaders" aria-hidden="true"></span></span>
      <span class="page"><span class="visually-hidden">Page</span> 1</span>
    </a>

    <ol role="list">
      <!-- subsection items -->
    </ol>
  </li>
</ol>

Und das CSS wird:

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

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

Jetzt werden die Punkte von Screenreadern ignoriert und die Benutzer vor der Frustration bewahrt, dutzende von Punkten angesagt zu bekommen.

Feinschliff

An diesem Punkt sieht die Inhaltsverzeichnis-Komponente ziemlich gut aus, aber sie könnte noch einige kleine Detailarbeiten gebrauchen. Zunächst einmal werden in den meisten Büchern Kapitelüberschriften optisch von Unterabschnitts-Überschriften abgesetzt, daher habe ich die obersten Elemente fett gedruckt und einen Rand eingefügt, um Unterabschnitte von den folgenden Kapiteln zu trennen.

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

Als Nächstes wollte ich die Ausrichtung der Seitenzahlen korrigieren. Alles sah gut aus, als ich eine Schriftart mit fester Breite verwendete, aber bei variablen Schriftbreiten konnten die Füllpunkte ein Zickzackmuster bilden, da sie sich an die Breite einer Seitenzahl anpassten. Zum Beispiel war jede Seitenzahl mit einer 1 schmaler als andere, was zu Füllpunkten führte, die nicht mit den Punkten auf vorherigen oder folgenden Zeilen übereinstimmten.

Misaligned numbers and dots in a table of contents.

Um dieses Problem zu lösen, habe ich font-variant-numeric auf tabular-nums gesetzt, damit alle Zahlen mit derselben Breite behandelt werden. Durch die Festlegung der Mindestbreite auf 2ch habe ich sichergestellt, dass alle Zahlen mit ein oder zwei Ziffern perfekt ausgerichtet sind. (Sie sollten dies möglicherweise auf 3ch setzen, wenn Ihr Projekt mehr als 100 Seiten hat.) Hier ist das endgültige CSS für die Seitenzahl:

.toc-list li > a > .page {
  min-width: 2ch;
  font-variant-numeric: tabular-nums;
  text-align: right;
}
Aligned leader dots in a table of contents.

Und damit ist das Inhaltsverzeichnis komplett!

Fazit

Das Erstellen eines Inhaltsverzeichnisses nur mit HTML und CSS war eine größere Herausforderung als erwartet, aber ich bin sehr zufrieden mit dem Ergebnis. Dieser Ansatz ist nicht nur flexibel genug, um Kapitel und Unterabschnitte unterzubringen, sondern er bewältigt auch Unter-Unterabschnitte problemlos, ohne das CSS ändern zu müssen. Der Gesamtansatz funktioniert auf Webseiten, auf denen Sie zu verschiedenen Inhaltsorten verlinken möchten, sowie in PDFs, bei denen das Inhaltsverzeichnis zu verschiedenen Seiten verlinken soll. Und natürlich sieht es auch im Druck gut aus, wenn Sie es jemals in einer Broschüre oder einem Buch verwenden möchten.

Ich möchte Julie Blanc und Christoph Grabo für ihre ausgezeichneten Blogbeiträge zur Erstellung eines Inhaltsverzeichnisses danken, da beide bei meinen Anfängen von unschätzbarem Wert waren. Ich möchte auch Sara Soueidan für ihr Feedback zur Barrierefreiheit während meines Projekts danken.