📑 Markdown TOC Generator
Build a table of contents from Markdown headings with GitHub-style anchors.
Last updated: June 9, 2026 · By Λ
By Λ · Updated June 9, 2026 · ~3 min read
Why I built this
Every long README I write needs a table of contents at the top. Doing it by hand means I have to update it every time I rearrange a section, and I always forget. This tool generates the TOC from the actual headings in the document, so a quick paste-and-replace keeps it accurate.
Anchor styles, explained
GitHub style: lowercase, spaces become hyphens, punctuation stripped, deduplicate by appending -1, -2, etc. Used by GitHub, GitHub Pages, MkDocs by default. This is the safest default.
GitLab style: similar but allows more characters. Use this if your Markdown will be rendered on GitLab.
Plain style: no anchor links, just bullet-indented text. Use this for simple navigation in chat or email.
Min/max depth tip
For READMEs, "min depth 2, max depth 3" is the sweet spot: skip the document title (H1), include sections (H2) and major subsections (H3), skip the leaf-level (H4-H6). For docs sites, "min 1, max 4" makes sense.
How the generator reads your Markdown
The script walks the document one line at a time. Lines starting with three backticks flip a fence flag, and anything inside a fenced block is ignored, so a bash comment like # install dependencies stays out of the result. A line counts as a heading when it opens with one to six hash marks plus a space; trailing closing hashes get trimmed. Headings outside your depth range are dropped, and the survivors are indented relative to the shallowest level that made the cut. The whole generator is the short script embedded in this page, meaning the Markdown you paste is parsed inside your own tab and never posted to a server.
Anchors come from the heading text: lowercase it, strip punctuation, and collapse whitespace runs into single hyphens. Duplicate headings get -1, -2 suffixes in order of appearance. Switching Numbered to Yes swaps the bullet for hierarchical counters (1., 1.1., 2.) that reset whenever the document steps back up a level.
Worked example
Paste this fragment with the default settings (min depth 2, max depth 4, GitHub anchors):
## Getting Started ### Install ### Install ## Command Reference
and the generated TOC is:
- [Getting Started](#getting-started) - [Install](#install) - [Install](#install-1) - [Command Reference](#command-reference)
The second Install picked up -1 automatically. With Numbered set to Yes the same headings come out as 1. Getting Started, 1.1. Install, 1.2. Install, 2. Command Reference, still as working links.
Edge cases to know about
First, only ATX headings (the # kind) are detected; setext headings underlined with === or --- are skipped. Second, fences are matched on backticks, so a hash line inside a ~~~ block would be misread as a heading. Third, punctuation between words collapses to one hyphen here, while GitHub keeps a hyphen per removed space: Results & Benchmarks becomes #results-benchmarks in this tool but #results--benchmarks on GitHub, so verify anchors for headings with mid-text symbols.
Frequently Asked Questions
Why does my TOC skip the document title?
Min depth defaults to 2 because most READMEs use a single H1 as the title. Set min depth to 1 to list that H1 too.
Can I include H5 and H6 headings?
Yes. Raise max depth to 5 or 6; each deeper level adds two more spaces of indentation.
Will bold or code formatting inside a heading break the link?
No. A heading written as **Important** generates [**Important**](#important): asterisks are stripped from the slug while the label still renders bold inside the link.
How do I refresh the TOC after editing my document?
Paste the revised Markdown and copy the new output over your old block; everything recalculates on each keystroke and option change.
Related
For full Markdown editing with preview, see the markdown editor. For Markdown-to-HTML, see the html-markdown converter. For tables, see the markdown table tool.