Markdown Anchor Links: Complete Guide to In-Page Navigation
When you're writing long documents in Markdown, you've probably wanted a table of contents where clicking a section name jumps straight to that part of the page. That's exactly what markdown anchors do.
The concept itself isn't complicated, but here's the catch: different platforms handle anchors differently. GitHub has one set of rules, GitLab has another, and Pandoc does its own thing. This article walks through all the approaches and the platform-specific differences you need to know.
What Are Markdown Anchors
Anchors come from HTML. You give an element an id, then link to it with #id. Markdown anchors work the same way, just with simpler syntax.
Here's the simplest example:
[Jump to Installation](#installation)
## Installation
Installation steps go here...Click "Jump to Installation" and the page scrolls to the ## Installation heading. Most Markdown renderers automatically generate an ID from the heading text, so you just need # plus that ID to link there.
By the way, if you want to learn about other types of links (external links, image links), check out the Markdown link syntax article. This one focuses specifically on anchor links.
Auto-Generated Heading Anchors
This is the most common approach and the default behavior on most platforms — write a heading, the renderer generates an ID, and you link to it with #id.
Basic Usage
## Quick Start
Click [here](#quick-start) to go back to the top.In this example, ## Quick Start gets rendered as an HTML heading with an id attribute, and you can link to it with #quick-start.
The ID generation rules vary across platforms though. Here's a comparison of the major ones:
| Rule | GitHub | GitLab | Pandoc | Jekyll |
|---|---|---|---|---|
| English heading | Getting Started → getting-started | Same as GitHub | Same | Same |
| Spaces | Replace with - | Same | Same | Same |
| Uppercase | Convert to lowercase | Same | Same | Same |
| Special characters | Remove (keep -) | Same | Same | Same |
| Duplicate headings | Append -1, -2 | Same | Same | Same |
GitHub's Heading ID Rules
GitHub is the most widely used Markdown platform, and its rules are fairly representative:
## What's New? → id="whats-new"
## Getting Started → id="getting-started"
## Let's try it! → id="lets-try-it"
## 3. Installation → id="3-installation"The rules: convert to lowercase, replace spaces with hyphens, strip punctuation (keep hyphens), keep numbers. If two headings produce the same ID, the second one gets -1 appended, the third gets -2, and so on.
I ran into this once when writing a README with two subsections both called "Configuration" — one under Linux and one under macOS. Both generated #configuration, so GitHub renamed the second one to #configuration-1. I couldn't figure out why my links were jumping to the wrong spot until I realized it was the duplicate heading issue. If you have same-named headings, give one a custom anchor.
Creating Anchors Manually
Auto-generated heading anchors cover most use cases, but sometimes you want to link to a position that isn't a heading — like a table, a code block, or a specific paragraph. That's when you need manual anchors.
Using HTML <a> or <span> Tags
Markdown doesn't have a built-in syntax for creating anchors, but almost every renderer supports inline HTML. The most universal approach is using the id attribute on an HTML tag:
<a id="important-note"></a>
> This is a critical note I want readers to jump to directly.
Click [here](#important-note) to jump to the note above.Both <a> and <span> work fine. I prefer <a> since it's semantically correct (it's literally an anchor). Some people use <div id="xxx">, which also works, but <div> is a block-level element and can affect layout in some cases.
One thing to watch out for: CSDN's editor strips the name attribute from <a> tags, keeping only id. So always use id on CSDN.
Custom Heading Anchor IDs
Some Markdown extensions let you specify a custom ID directly on a heading, bypassing the auto-generation rules.
The {#id} Syntax
This comes from Markdown Extra and Pandoc — it's the most common custom ID syntax:
## Installation Steps {#install-steps}
Detailed installation content...
[Jump to Installation Steps](#install-steps)The {#install-steps} goes after the heading text. No matter what the heading says, the anchor ID will be install-steps.
Support varies:
- Pandoc: Full support
- Kramdown (Jekyll default): Full support
- PHP Markdown Extra: Full support
- R Markdown: Supported
- GitHub: Not supported —
{#id}gets treated as part of the heading text
GitHub not supporting {#id} catches a lot of people off guard. If you need custom anchors on GitHub, use the HTML <a id="xxx"> approach instead.
Full Anchor Link Syntax
Now that you know how to create anchors, let's look at how to link to them.
In-Page Jumps
The most basic usage — jump within the current page:
[Jump to FAQ](#faq)
... (lots of content) ...
## FAQThe link address is just # plus the target ID. No page path needed.
Cross-Page Jumps
Anchors can also jump to a specific position on another page:
[See installation in the next article](./setup.md#linux)
[Check the official API docs](https://example.com/docs#api-reference)Just append #id to the URL. For local Markdown files, use a relative path plus #. For web URLs, just tack # on the end.
I used this a lot when writing a tutorial series — each article had a "previous / next" navigation at the top, plus a link that jumped straight to a specific section in the next article. Cross-page anchors are really handy for multi-document projects.
Using HTML <a> Tags for Links
You can also write anchor links with raw HTML:
<a href="#faq">Jump to FAQ</a>Same effect as the Markdown syntax. On platforms that don't support Markdown anchor links, the HTML version sometimes works when Markdown doesn't.
Platform Compatibility Differences
This is the most frustrating part of working with markdown anchors. Here are the key differences across platforms I've actually used:
| Feature | GitHub | GitLab | CSDN | Jekyll | Pandoc |
|---|---|---|---|---|---|
| Auto heading IDs | ✅ | ✅ | ✅ | ✅ | ✅ |
{#id} custom | ❌ | ✅ | ❌ | ✅ | ✅ |
<a id=""> manual | ✅ | ✅ | ✅ (id only) | ✅ | ✅ |
<a name=""> | ❌ | ✅ | ❌ (filtered) | ✅ | ✅ |
| Cross-page anchors | ✅ | ✅ | ✅ | ✅ | ✅ |
The pattern here: <a id="xxx"> is the most universal approach. It works everywhere. If you're not sure what platform your content will be rendered on, use this method.
Common Issues and Troubleshooting
Anchor Links Not Working
The most common problem. Check these things:
ID mismatch: The ID in your link must match the target exactly, including case.
#Getting-Startedand#getting-startedare different.Platform support: Make sure your platform supports auto-generated heading IDs. Some older editors don't.
Check rendered output: Anchors only work in rendered HTML, not in raw Markdown source. If your editor has a preview mode, switch to that.
Spaces and special characters: If you're linking to a heading with spaces or punctuation, convert it according to the platform's rules. On GitHub,
## Hello World!becomes#hello-world, not#hello-world!.
Duplicate Headings
As mentioned earlier, most platforms append numeric suffixes to duplicate headings:
## Configuration → #configuration
## Configuration → #configuration-1
## Configuration → #configuration-2Not every platform handles this the same way. It's best to avoid duplicate headings or use custom anchors to differentiate them.
Hands-On: Building a Clickable Table of Contents
Now let's put it all together and create a clickable table of contents for a document:
## Table of Contents
- [Introduction](#introduction)
- [Installation](#installation)
- [Configuration](#configuration)
- [FAQ](#faq)
## Introduction
Intro content...
## Installation
Install steps...
## Configuration
Config details...
## FAQ
Questions and answers...Simple as that — a Markdown list with anchor links.
For long documents, add a "back to top" link at the end of each section:
## Introduction
Intro content...
[↑ Back to TOC](#table-of-contents)The syntax itself is straightforward, but the gotchas are in the details. Platform differences, special character handling, duplicate heading suffixes — those are the things that'll trip you up. My advice: figure out which platform you primarily use, learn its rules, and stick with <a id="xxx"> if you need cross-platform compatibility.
References
- GitHub Flavored Markdown Spec — Official GitHub Markdown specification
- Markdown Guide — Hacks — Markdown Guide project, anchor workaround section
- CommonMark Spec Discussion: Anchors in Markdown — Standard-level discussion about anchor support
- Pandoc User's Guide — Heading Identifiers — Pandoc's heading ID documentation
- markdown-it-anchor (npm) — Developer-side anchor rendering reference