How to link to a heading in markdown

Updated

To link to a heading in markdown, use a normal link whose target is # followed by the heading's slug: the heading text in lower case, spaces replaced by hyphens, punctuation removed:

### Getting started

See [the setup steps](#getting-started) first.

See the setup steps first.

Renderers generate the id for every heading automatically; you never write it. What you do need is the exact slug rule of the renderer you publish on, because "Getting Started: Part 2" becomes #getting-started-part-2 on GitHub and something slightly different on a few other platforms. The TOC generator produces GitHub-exact anchors for a whole document if you would rather not derive them by hand.

How does GitHub turn a heading into an anchor?

GitHub's section links docs give the rules, and they are implemented in the open-source github-slugger package, so you can reproduce them exactly:

RuleHeadingAnchor
Letters are lower-cased## Getting Started#getting-started
Spaces become hyphens## Install on Linux#install-on-linux
Punctuation is removed (not replaced)## What is it?#what-is-it
Hyphens and underscores are kept## pre-commit_hook#pre-commit_hook
Dots are removed, so numbers merge## v1.0.2 release#v102-release
Inline markup is stripped## Use _italics_#use-italics
Emoji and symbols are removed, leaving their hyphen## 🚀 Getting started#-getting-started
Accented letters survive## über café#über-café
Leading and trailing spaces are trimmed## Padded #padded
Duplicates get -1, -2, in document ordersecond ## Notes#notes-1

Apostrophes and slashes are punctuation too: ## Don't panic is #dont-panic and ## API/CLI is #apicli. The duplicate counter applies to the slug, not the text, so ## Setup and ## Setup! collide and the second one gets -1. If you insert a third heading with the same text above the others, the suffixes shift and existing links move; GitHub's docs warn about exactly this.

Do other platforms use the same anchor rules?

Mostly, with small differences that only show up on punctuation, emoji and duplicates. The common core (lower case, spaces to hyphens, punctuation dropped) is the same everywhere:

PlatformDiffers from GitHub inSource
GitLab. is removed (3.5 is 35), emoji shortcodes become their name (:thumbsup: is thumbsup), snippet anchors get a filename prefixGitLab heading IDs
ObsidianLinks use the heading text itself: [[Note#Getting started]], no sluggingObsidian syntax
DocusaurusSame rules plus an explicit {#custom-id} suffix on the headingDocusaurus headings docs
MkDocs (Python-Markdown toc)Punctuation removed; duplicates get _1 with an underscorePython-Markdown toc extension
Jekyll (kramdown)Duplicates get -1; leading digits are stripped, so ## 2. Setup is #setupkramdown syntax
PandocLeading digits and punctuation stripped, section if nothing remainsPandoc manual
NotionNo markdown anchors; block links are generated by the appNotion's import docs say anchor links may not import cleanly
Slack, Discord, RedditNo headings with ids, so no anchors

The safe habit is to write headings that slug identically everywhere: plain words, no leading numbers, no punctuation that matters, no emoji. ## Install on Linux is #install-on-linux on every platform in the table except Obsidian, which does not slug at all.

Put the anchor after the file path: [Setup](docs/install.md#getting-started). On GitHub a relative path resolves against the current file and follows the branch you are viewing, and the fragment is applied after the page loads:

See [configuration](./CONFIGURATION.md#environment-variables)
and the [contributing guide](../CONTRIBUTING.md#running-tests).

Three rules from GitHub's relative links docs: paths starting with / are relative to the repository root, ./ and ../ work as in a shell, and the link text must be on one line. Absolute URLs (https://github.com/org/repo/blob/main/README.md#usage) also work but break in forks and clones, so prefer relative. The link guide has the rest of the link syntax.

How do you make a custom anchor that does not depend on the heading text?

Put an empty HTML anchor where you want the target, then link to its id:

<a id="install"></a>
### Installation on every operating system we support

Jump to [install](#install).

GitHub allows <a> with id (and name) through its HTML sanitiser, so this renders and the link works. The anchor survives heading rewrites, which is the point: the auto slug changes every time someone edits the heading, the explicit id does not. Keep the <a> on its own line directly above the heading; putting it inside the heading text would get the id but also add the tag to the slug GitHub generates. Obsidian, Typora and most static-site generators pass raw HTML through, so the pattern is portable. Docusaurus and MkDocs offer ## Heading {#install} instead, which is cleaner but prints the braces literally on GitHub.

Nine times out of ten the slug is slightly wrong. Check these, in order:

  1. Case. Anchors are lower case; #Getting-Started does not match #getting-started on GitHub.
  2. Punctuation. You replaced it with a hyphen instead of dropping it: ## FAQ: billing is #faq-billing, not #faq--billing, because the colon is removed and the single space becomes one hyphen.
  3. Duplicate heading. The heading you want is the second one with that text; it is #notes-1.
  4. Wrong platform. The document is rendered by a tool with different rules (see the platform table) or by one that has no anchors at all.
  5. Markup in the heading. Code spans and links are stripped: ## Using \npm`is#using-npm`.
  6. The heading is in a collapsed section. On GitHub, a heading inside <details> has an id, but the browser cannot scroll to hidden content until the section is opened.

Run the file through the TOC generator and copy the anchor it prints; it uses the same slugger GitHub does. The markdown viewer renders headings with ids so you can test the link in place.

Not in markdown. Anchors are generated for headings only; paragraphs, list items and table rows get no id. The options are to add a heading where you need a target, to insert a manual <a id="..."></a> (which works on GitHub and in most HTML-tolerant renderers), or, for source files on GitHub, to link to the file view with a line fragment (blob/main/file.md?plain=1#L42), which is a GitHub URL feature rather than markdown. On GitHub you can also click a rendered heading to get the link icon and copy the exact anchor.

How should you write headings so the anchors stay stable?

Use plain words, keep headings unique within the file, avoid leading numbers and trailing punctuation, and do not rename a heading that other documents link to without searching for the old anchor. If a heading must change, leave a <a id="old-slug"></a> above the new heading so inbound links keep working. The headings guide covers the heading syntax itself, and the markdown cheat sheet has the rest of the syntax on one page.

Keep going