Documentation
Home · Docs · Markdown · Markdown table of contents

Markdown table of contents

Build a table of contents from anchor links, or let your platform generate one.

To create a table of contents in Markdown, write a list of links whose destinations are the heading anchors, such as [Usage](#usage). Standard Markdown has no table of contents syntax, so the manual list is the method that works everywhere headings get anchors. Some tools add an automatic marker: GitLab accepts [[_TOC_]] and [TOC], and the Python-Markdown toc extension replaces [TOC]. GitHub does not support a marker, but it shows a generated outline for every Markdown file.

Markdown table of contents reference

GoalSyntaxRenders as
Link to a heading[Usage](#usage)A link that jumps to the ## Usage heading
Nested entry  - [Requirements](#requirements)An indented sub-item under the previous entry
Automatic TOC (GitLab)[[_TOC_]]A generated list of links to the document's headings
Automatic TOC (Python-Markdown)[TOC]A nested list of headings, with the toc extension enabled
Link to a duplicate heading[Notes](#notes-1)A link to the second heading named Notes on GitHub

Key facts

  • CommonMark and GitHub Flavored Markdown have no table of contents syntax. A manual TOC is a list of links to heading anchors.
  • GitHub builds heading anchors by lowercasing the text, turning spaces into hyphens, and removing other punctuation.
  • On GitHub, a repeated heading gets a hyphen and a counter, so the second ## Notes is #notes-1.
  • GitHub does not render [TOC], but it shows a generated table of contents through the Outline button on Markdown files.
  • GitLab renders [[_TOC_]] or [TOC] on its own line in Markdown files, wikis, issues, merge requests and epics, but not in comments.
  • Python-Markdown replaces [TOC] when the toc extension is enabled. Pandoc builds a TOC with the --toc option instead of a marker.
  • Obsidian has no [TOC] marker. Its Outline view lists the headings of the active note.

A manual table of contents is an ordinary Markdown list in which every item is a link. The link destination is # followed by the anchor of the heading you want to jump to. Renderers that add anchors to headings, such as GitHub and GitLab, make each link scroll to its section.

A flat table of contents

Markdown

## Contents

- [Installation](#installation)
- [Usage](#usage)
- [Configuration](#configuration)
- [License](#license)

## Installation
...

## Usage
...

Rendered output

Contents

Installation

...

Usage

...

HTML output

<h2>Contents</h2>
<ul>
<li><a href="#installation">Installation</a></li>
<li><a href="#usage">Usage</a></li>
<li><a href="#configuration">Configuration</a></li>
<li><a href="#license">License</a></li>
</ul>
<h2 id="installation">Installation</h2>
<p>...</p>
<h2 id="usage">Usage</h2>
<p>...</p>

The renderer adds the id attributes to the headings. You only write the links. The exact id format can vary by renderer, so test the links in the final destination.

Put the table of contents near the top of the document, after the title and a short introduction. A heading such as ## Contents above the list helps readers and screen reader users find it. Because the list is plain Markdown, it renders as a readable list of links even in tools that do not add heading anchors.

How GitHub generates heading anchors

Your links only work if they match the anchor the renderer generates. GitHub documents its rules for section links:

  • Letters are converted to lowercase.
  • Spaces are replaced by hyphens. Other whitespace and punctuation characters are removed.
  • Leading and trailing whitespace is removed, and formatting such as **bold** is stripped so only the text remains.
  • If the same anchor already exists in the document, a hyphen and an incrementing number are appended: -1, -2 and so on.

Punctuation and duplicate headings

Markdown

- [What's new in v2.0?](#whats-new-in-v20)
- [Notes](#notes)
- [Notes (second)](#notes-1)

## What's new in v2.0?
## Notes
## Notes

Rendered output

What's new in v2.0?

Notes

Notes

HTML output

<ul>
<li><a href="#whats-new-in-v20">What's new in v2.0?</a></li>
<li><a href="#notes">Notes</a></li>
<li><a href="#notes-1">Notes (second)</a></li>
</ul>
<h2 id="whats-new-in-v20">What's new in v2.0?</h2>
<h2 id="notes">Notes</h2>
<h2 id="notes-1">Notes</h2>

The apostrophe, the period and the question mark are removed, so What's new in v2.0? becomes whats-new-in-v20.

HeadingGitHub anchor
## Getting Started#getting-started
## API & SDK#api--sdk
## Step 1: Install#step-1-install
## **Bold** heading#bold-heading

In API & SDK, the ampersand is removed but both spaces become hyphens, which leaves a double hyphen. The quickest way to check an anchor on GitHub is to hover over the heading and click the link icon, which puts the anchor in the address bar. GitLab uses similar rules, but other renderers differ, especially for non-Latin characters and emoji. Python-Markdown, for example, suffixes duplicates with an underscore (header_1). For explicit anchors, see heading IDs.

Nested table of contents

To show subsections, indent the child links under their parent item, the same way you nest any Markdown list. Indent the child line so its marker lines up with the text of the parent item: two spaces under -, three under 1..

Numbered parents with bulleted subsections

Markdown

## Table of contents

1. [Getting started](#getting-started)
   - [Requirements](#requirements)
   - [Install on macOS](#install-on-macos)
2. [Writing documents](#writing-documents)
   - [Headings and lists](#headings-and-lists)
   - [Code blocks](#code-blocks)
3. [FAQ](#faq)

Rendered output

HTML output

<h2>Table of contents</h2>
<ol>
<li><a href="#getting-started">Getting started</a>
<ul>
<li><a href="#requirements">Requirements</a></li>
<li><a href="#install-on-macos">Install on macOS</a></li>
</ul>
</li>
<li><a href="#writing-documents">Writing documents</a>
<ul>
<li><a href="#headings-and-lists">Headings and lists</a></li>
<li><a href="#code-blocks">Code blocks</a></li>
</ul>
</li>
<li><a href="#faq">FAQ</a></li>
</ol>

Most tables of contents list only ## and ### headings. Listing every level makes the TOC long enough that readers skip it. Skip the document title (#) as well, since the reader is already at the top.

Automatic TOC: [TOC] and [[_TOC_]]

A table of contents marker is a placeholder that the renderer replaces with a generated list of headings. It is an extension, not part of CommonMark or GitHub Flavored Markdown. In a renderer that does not support it, the marker shows as literal text.

[[_TOC_]]

## Overview
...

## Setup
...
  • GitLab renders [[_TOC_]] or [TOC] when it is on its own line. It works in Markdown files, wiki pages, issues, merge requests and epics, but not in comments or notes.
  • Python-Markdown (used by MkDocs and other static site tools) replaces [TOC] when the toc extension is enabled. The toc_depth option controls which heading levels appear, and the marker option changes or disables the placeholder.
  • Pandoc has no in-document marker. Run it with --toc (or --table-of-contents) and --toc-depth to add a generated TOC to the output.
  • GitHub does not replace [TOC] or [[_TOC_]]. Write a manual list or rely on the Outline button.

Built-in outlines: GitHub, Obsidian and VS Code

Several tools show a table of contents in their interface without changing the file. This helps the reader of that tool, but the TOC does not travel with the document.

  • GitHub generates a table of contents from the headings of a README or other Markdown file. Click the Outline icon in the file header to open it.
  • Obsidian has a core Outline view that lists the headings in the active note and jumps to a section when you click it. There is no [TOC] marker. To build an in-note TOC, write a list of heading links such as [[#Setup]].
  • VS Code lists headings in the Outline view of the Explorer. To insert a TOC into the file itself, extensions such as Markdown All in One add a Create Table of Contents command that writes a linked list and updates it on save.

If the document will be read in more than one place, such as a README that is also published to a docs site, a manual list in the file is the most portable option. An editor extension can generate and maintain it for you.

Generated anchors change when you edit the heading text, which breaks the TOC link. If a heading is long or likely to change, give it an explicit ID and link to that. GitHub has no {#id} syntax, so use a raw HTML heading where inline HTML is allowed. Pandoc and Markdown Extra accept ## Heading {#id}.

A short, stable anchor for a long heading

Markdown

- [Install](#install)

<h2 id="install">Installation steps (macOS, Windows and Linux)</h2>

Rendered output

Installation steps (macOS, Windows and Linux)

HTML output

<ul>
<li><a href="#install">Install</a></li>
</ul>
<h2 id="install">Installation steps (macOS, Windows and Linux)</h2>

A raw HTML heading is not parsed as Markdown, so **bold** or links inside it render as literal text. See heading IDs and inline HTML.

Flavor differences

Heading anchors and TOC markers are both renderer features. The core specs define neither.

Flavor or platformTOC markerNotes
CommonMarkNoNo TOC syntax and no heading IDs in the spec. Write a manual list of links.
GitHub (GFM)NoAuto-generates heading anchors. Shows a generated TOC through the Outline button on Markdown files.
GitLabYes: [[_TOC_]] or [TOC]Markdown files, wikis, issues, merge requests and epics. Not in comments.
Python-Markdown (toc extension)Yes: [TOC]Marker text is configurable. Duplicate IDs use an underscore suffix such as _1.
PandocNoUse the --toc command-line option. Supports {#id} for explicit anchors.
ObsidianNoOutline view lists headings. Link to headings with [[#Heading]].
VS CodeNoOutline view in the Explorer. Extensions can insert a TOC list into the file.

Behavior can change between versions and settings, so check your renderer before relying on a marker. For a wider comparison, see Markdown flavors.

For the full comparison of how flavors differ across all elements, see Markdown flavors.

How Markdific renders it

Markdific renders a manual table of contents as a standard list of links, and nested lists keep their indentation. A [TOC] or [[_TOC_]] marker is not part of CommonMark or GFM, so check the preview before relying on one. Try it in the Markdific online editor.

Common mistakes and gotchas

  • Using the heading text as the link. [Usage](#Getting Started) does not work. Use the lowercase, hyphenated anchor: #getting-started.
  • Keeping punctuation in the anchor. GitHub removes characters such as ?, ., : and apostrophes, so ## What's new? becomes #whats-new.
  • Expecting [TOC] to work on GitHub. GitHub shows the marker as literal text. Use a manual list or the Outline button.
  • Forgetting duplicate suffixes. The second heading with the same text gets -1 on GitHub. A link to the plain anchor goes to the first one.
  • Not updating the TOC after renaming a heading. The anchor changes with the text, and the old link silently stops working.
  • Under-indenting nested items. If a child line is not indented to the parent's text, it becomes a separate top-level item.

Best practices

  • Write a manual TOC for documents read on several platforms. It renders as a list of links everywhere.
  • Include only ## and ### headings unless the document is very long.
  • Keep headings unique so anchors stay predictable and need no numeric suffix.
  • Check each anchor in the final destination, for example by clicking the heading link icon on GitHub.
  • Use an editor extension to generate and update the TOC so it stays in sync with the headings.
  • Give long or frequently edited headings an explicit ID so TOC links survive wording changes.

HTML equivalent

A table of contents compiles to a list of fragment links. The headings carry matching id attributes, which the renderer adds.

Markdown

- [Overview](#overview)
- [Setup](#setup)

## Overview

## Setup

HTML output

<ul>
<li><a href="#overview">Overview</a></li>
<li><a href="#setup">Setup</a></li>
</ul>
<h2 id="overview">Overview</h2>
<h2 id="setup">Setup</h2>

FAQ

How do I create a table of contents in Markdown? Write a list of links that point to your heading anchors, for example - [Usage](#usage). Indent links to subsections to nest them. Standard Markdown has no TOC syntax, but this list works in any renderer that adds heading anchors.

Does GitHub support [TOC] in Markdown? No. GitHub shows [TOC] as plain text. It generates a table of contents for Markdown files that you open with the Outline icon in the file header, and you can write a manual list of anchor links in the file.

How are heading anchors generated on GitHub? GitHub lowercases the heading, replaces spaces with hyphens, removes other punctuation and formatting, and appends -1, -2 to repeated anchors. ## Getting Started becomes #getting-started.

What is the difference between [TOC] and [[_TOC_]]? Both are table of contents markers. GitLab documents [[_TOC_]] and also renders [TOC]. Python-Markdown's toc extension uses [TOC]. Neither works on GitHub or in plain CommonMark.

How do I make a table of contents in Obsidian? Obsidian has no [TOC] marker. Open the core Outline view to see and jump between the headings of the active note, or write a list of heading links such as [[#Setup]] in the note.

Why does my table of contents link not work? The anchor usually does not match the generated one. Check for uppercase letters, kept punctuation, a renamed heading or a duplicate heading that needs a -1 suffix. On GitHub, hover over the heading and click the link icon to see the real anchor.

Can VS Code generate a Markdown table of contents? Not by itself, though its Outline view lists headings. Extensions such as Markdown All in One add a Create Table of Contents command that inserts a linked list and updates it when you save.

Sources and compatibility references

Use these primary references for syntax rules. Renderer behavior can still vary by version and configuration, so preview important documents in their final destination.

To see how your Markdown renders while you write, open the file in Markdific, a Markdown viewer and editor for Mac, Windows and Linux.