Skip to main content

Table of Contents Guidelines

1. Introduction​

This guide explains how to create a printed Table of Contents (TOC) in templates rendered by WeasyPrint and PrinceXML.

A Table of Contents is a page inside the PDF. It lists document sections and shows the page number for each section.

The goal of this guide is to help template developers:

  • Build a TOC with real page numbers.
  • Keep the TOC updated when content changes.
  • Control indentation for section levels.
  • Avoid common mistakes.
info

PDF bookmarks and a printed Table of Contents are different features. Bookmarks appear in the PDF viewer sidebar. The TOC appears as a page in the document. See Bookmarks for the sidebar outline.


2. Basic Concept​

Render engines don't have a built-in "insert TOC" command. You build the TOC with normal HTML links and a few CSS functions.

The idea is simple:

  1. Give every heading an id.
  2. Add one empty link per heading, pointing to that id.
  3. Let CSS fill in the title, the dots, and the page number.

Because CSS reads the title and page number from the heading, the TOC stays correct. If a heading moves to another page, the number updates on the next render.

FunctionWhat it returns
target-text(attr(href))The text of the linked heading
target-counter(attr(href), page)The page number of that heading
leader('.')A row of dots that fills the empty space

These functions work in the PDF. They do not work in normal browsers such as Chrome or Firefox.


3. Minimal Example​

<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<style>
h1 {
break-before: page;
}

.toc-entry a::before {
content: target-text(attr(href));
}

.toc-entry a::after {
content: ' ' leader('.') ' ' target-counter(attr(href), page);
}
</style>
</head>
<body>
<nav>
<p class="toc-entry"><a href="#intro"></a></p>
<p class="toc-entry"><a href="#method"></a></p>
</nav>

<h1 id="intro">Introduction</h1>
<p>Some text.</p>

<h1 id="method">Methodology</h1>
<p>Some more text.</p>
</body>
</html>

Result in the PDF:

Introduction ..................................... 2
Methodology ...................................... 3

Important​

Keep the links empty. If you put text inside <a>, the title appears twice.


4. Indentation by Level​

Use a class for each TOC level. This is easier to control than nested lists, and the dots stay aligned.

<nav id="toc">
<h1 class="toc-title">Table of Contents</h1>

<p class="toc-entry toc-l1"><a href="#introduction"></a></p>
<p class="toc-entry toc-l2"><a href="#background"></a></p>
<p class="toc-entry toc-l2"><a href="#scope"></a></p>
<p class="toc-entry toc-l1"><a href="#methodology"></a></p>
</nav>

<main>
<h1 id="introduction">Introduction</h1>
<h2 id="background">Background</h2>
<h2 id="scope">Scope</h2>
<h1 id="methodology">Methodology</h1>
</main>
.toc-entry {
margin: 0 0 0.35em 0;
}

.toc-entry a {
text-decoration: none;
color: inherit;
}

.toc-entry a::before {
content: target-text(attr(href));
}

.toc-entry a::after {
content: ' ' leader('.') ' ' target-counter(attr(href), page);
}

.toc-l2 {
margin-left: 1.2em;
font-size: 10.5pt;
}

.toc-l3 {
margin-left: 2.4em;
font-size: 10pt;
}

Result:

Introduction ..................................... 2
Background ................................... 3
Scope ........................................ 4
Methodology ...................................... 5
  • Use toc-l1 for h1 sections.
  • Use toc-l2 for h2 sections.
  • Use toc-l3 for h3 sections.

5. Table-Style Layout​

Some reports need columns such as number, topic, and page. Use a normal HTML table.

<table class="toc">
<tr>
<th>No.</th>
<th>Topic</th>
<th>Page</th>
</tr>
<tr>
<td>1</td>
<td><a href="#scan-overview">Scan Overview</a></td>
<td class="page-number"><a href="#scan-overview"></a></td>
</tr>
</table>
.page-number a::before {
content: target-counter(attr(href), page);
}

In this layout:

  • The topic cell has a normal link with visible text.
  • The page cell has an empty link. CSS fills in the page number.

6. Numbered Sections​

If your headings show numbers such as 1, 1.1, or 1.1.1, you can show the same numbers in the TOC.

h1 {
counter-increment: sec;
counter-reset: sub;
}

h1::before {
content: counter(sec) '. ';
}

h2 {
counter-increment: sub;
}

h2::before {
content: counter(sec) '.' counter(sub) ' ';
}

.toc-entry a::before {
content: target-counters(attr(href), sec, '.') ' ' target-text(attr(href));
}

Put counter-increment on the heading itself. Do not put it on a wrapper element, or the TOC number may be wrong.


7. Common Problems​

The page number is empty.

The href does not match any id, or the CSS selector does not match the link. Check the spelling of both.

The heading title appears twice.

The link is not empty. Remove the text inside <a> and let target-text() supply it.

The whole entry disappeared.

You may have set content on the <a> element itself. Set it on ::before or ::after instead.

The dots do not appear or do not stretch.

leader() needs a block element with a known width. Prefer a simple <p> or a table cell. Avoid flex layouts around the entry.

Page numbering does not restart after the cover.

counter-reset: page 1 only works inside an @page rule:

@page toc {
counter-reset: page 1;
}

8. Checklist​

  • Every TOC entry points to an id that exists.
  • TOC links are empty (no text inside <a>).
  • Each entry has a level class (toc-l1, toc-l2, toc-l3).
  • Page counter reset, if used, is inside an @page rule.
  • You opened the PDF and clicked a few entries to confirm the links.