Markdown Heading Syntax
How to create Markdown headings. Covers ATX # syntax and setext underlines, heading anchors and ids, hierarchy, table of contents, and the mistakes that break them.
Basic Syntax
ATX headings use one to six hash characters at the start of a line. The number of hashes sets the level. Always follow the hashes with a space.
# Heading 1
## Heading 2
### Heading 3
#### Heading 4
##### Heading 5
###### Heading 6Heading 1
Heading 2
Heading 3
Heading 4
Heading 5
Heading 6
Setext Headings
The original Markdown syntax supports level 1 and level 2 headings by underlining text with = or -. These are called setext headings. They are valid CommonMark, but less common today because they only cover two levels.
Heading 1
=========
Heading 2
---------Heading 1
Heading 2
Heading Hierarchy
A well-structured document uses one H1 for the main title, then H2s for sections, H3s for subsections, and so on. Do not skip levels: H1 to H3 without an H2 creates a broken outline and hurts accessibility.
Recommended outline
- # Page title
- ## Section
- ### Subsection
- ### Subsection
- ## Section
Anchors & IDs
Most modern renderers auto-generate an id for each heading so you can link directly to it. The id is usually the heading text lowercased, with spaces and punctuation replaced by hyphens.
## Installation
[Jump to installation](#installation)Installation
<h2 id="install">Installation</h2>Installation
To see the exact slug a renderer creates, check the id attribute in the generated HTML. Avoid special characters and keep headings concise so slugs stay predictable.
Table of Contents
A manual table of contents is a list of links to heading ids. It works in any renderer that generates heading anchors, including GitHub, GitLab, Obsidian, and static-site generators.
- [Overview](#overview)
- [Installation](#installation)
- [Usage](#usage)
## Overview
Intro paragraph.
## Installation
Steps to install.
## Usage
How to use it.Overview
Intro paragraph.
Installation
Steps to install.
Usage
How to use it.
If you maintain a large docs site, consider a static-site plugin or CI step that regenerates the TOC so headings and links do not drift out of sync.
Common Mistakes
Heading errors are usually spacing, level count, or hierarchy issues. Each row shows the broken version and the corrected one side by side.
Broken
#HeadingFixed
# HeadingBroken
####### Too many levelsFixed
###### Maximum six levelsBroken
# Heading 1
### Heading 3Fixed
# Heading 1
## Heading 2
### Heading 3Broken
## My Heading {#my-heading}Fixed
## My Heading
<h2 id="my-heading">My Heading</h2>Quick Reference
| Syntax | Level | Notes |
|---|---|---|
| # Heading | H1 | ATX syntax; most common |
| ## Heading | H2 | ATX syntax |
| ###### Heading | H6 | Maximum ATX level |
| Heading\n========= | H1 | Setext syntax |
| Heading\n--------- | H2 | Setext syntax |
| [Link](#heading) | - | Link to auto-generated heading slug |
| <h2 id="x">Heading</h2> | - | Custom id via raw HTML |
Platform Support
Headings are part of standard Markdown, so they work in most document renderers. Chat apps do not support them.
| Platform | Supported? | Notes |
|---|---|---|
| GitHub | Yes | ATX and setext headings work. GitHub Flavored Markdown auto-generates slug ids from heading text. |
| GitLab | Yes | Same as GitHub Flavored Markdown. |
| Yes | Supports ATX headings in posts and comments. | |
| VS Code preview | Yes | Renders ATX and setext headings. Markdown preview outline view uses the heading hierarchy. |
| Obsidian | Yes | ATX headings turn into outline nodes. Obsidian auto-generates heading block links such as #^heading. |
| Notion | Partial | Pasting Markdown converts # into Notion heading blocks. Live Markdown typing is not supported. |
| Discord | No | Discord messages do not support headings. |
| Slack | No | Slack mrkdwn does not support headings. |
| CommonMark | Yes | Defines ATX headings (# to ######) and setext headings (underlined with = or -). |
| Docusaurus / VitePress / Astro | Yes | Static-site generators render headings and often auto-generate ids and table-of-contents components. |
Frequently Asked Questions
How do you create headings in Markdown?
Use ATX syntax: one to six hash characters followed by a space and the heading text, like # Heading 1 or ## Heading 2. You can also use setext syntax: underline text with = for a level-1 heading or - for a level-2 heading.
How many heading levels does Markdown support?
Markdown supports six heading levels, from H1 (#) to H6 (######). Seven or more hashes render as plain text, not a heading.
What is the difference between ATX and setext headings?
ATX headings use hash characters at the start of the line and support all six levels. Setext headings are underlined with = or - and only support level 1 and level 2. ATX is more common because it is compact and supports every level.
How do I link to a heading in Markdown?
In most renderers, write a link to the heading's slug: [see installation](#installation) targets ## Installation. On GitHub, GitLab, and many static-site generators, the slug is auto-generated from the heading text, usually lowercased and with spaces replaced by hyphens.
How do I add a custom id to a Markdown heading?
Standard Markdown does not define custom heading ids. For a custom id, write the heading in raw HTML: <h2 id='install'>Installation</h2>. Most HTML-enabled renderers (GitHub, GitLab, Obsidian, VS Code, static-site generators) honor the id. Discord and Slack strip inline HTML, so this trick does not work there.
Can I skip heading levels?
Markdown syntax allows skipping levels, but you should not. A document should move from H1 to H2 to H3 in order. Skipping levels breaks the outline for screen readers, table-of-contents generators, and search crawlers.
How do I make a table of contents in Markdown?
Create a list of links to the auto-generated heading slugs. For example, - [Overview](#overview) links to ## Overview. Many static-site generators can build this automatically; GitHub does not auto-generate a TOC in rendered files.
Do Discord and Slack support Markdown headings?
No. Discord and Slack do not render headings. If you paste # Heading into a message, it appears as literal text. Use bold or all-caps for visual hierarchy in chat instead.
Related Markdown Guides
Headings are the backbone of document structure. For the surrounding syntax and platform guides: