---
title: "Markdown custom heading id: brace ids under pandoc"
slug: markdown-custom-heading-id
excerpt: "Markdown custom heading id syntax put to a pandoc test: the section Setting an id in braces with the markdown reader quotes the id it printed."
quickAnswer: "The section Heading ids in the CommonMark spec lists grep results from the 0.31.2 text beside a control. Quoted HTML from the test file hid.md sits in Setting an id in braces with the markdown reader. Output with header_attributes taken out is quoted under When the header attributes extension is off. Two commonmark runs of that same file sit under Commonmark with and without the attributes extension. A heading with a class and an id meets four reader settings in Adding a class next to the id. Laid out in Check a heading id with pandoc are six numbered steps, and grep runs in steps 3 and 6."
keyTakeaways:
  - "Grep results on the spec text for brace and extension strings, plus a control, are listed in Heading ids in the CommonMark spec."
  - "Quoted output under Setting an id in braces with the markdown reader shows the heading id and the link that targets it."
  - "With header_attributes removed, the heading and its id are quoted in When the header attributes extension is off."
  - "Output from two commonmark runs of hid.md appears in Commonmark with and without the attributes extension."
  - "Class and id output for one heading line is compared in Adding a class next to the id."
publishedDate: 2026-10-20
dateModified: 2026-10-20
author: Bobby Huang
cluster: B
role: post
status: published
faq:
  - q: What syntax gives a Markdown heading a custom id?
    a: "Brace syntax after the heading text, and the id pandoc printed for it, are shown in Setting an id in braces with the markdown reader."
  - q: "Why does my {#id} show up in the heading text?"
    a: "Under Commonmark with and without the attributes extension, two reader runs of hid.md are quoted."
  - q: Can a heading have a class as well as an id?
    a: "HTML for a heading with both is compared across four reader settings in Adding a class next to the id."
  - q: Which id did pandoc give my heading?
    a: "Steps that count ids and classes in pandoc's HTML for the test files are listed in Check a heading id with pandoc."
related:
  - markdown-headings-anchor-links
  - markdown-table-of-contents
  - markdown-links-to-local-files
---

You want a link that lands on one heading, and you'd like to pick the name it points at yourself. This post puts one way of writing that name to a pandoc 3.8.3 test: a markdown custom heading id typed inside a pair of braces after the heading words.

Two small test files went through pandoc on macOS under four reader settings, and `pandoc --version` showed 3.8.3. The output shows which settings turned the braces into an id and which kept them as plain heading text. What follows is only what pandoc printed in this test; no viewer's handling of that output was checked.

## Heading ids in the CommonMark spec

Ids that pandoc makes on its own, and the suffix it adds to a repeated heading, are covered in the [heading links guide](markdown-headings-anchor-links.md). The question here is smaller: does CommonMark 0.31.2 give you a way to write an id by hand?

Grep found no such syntax in the spec text. On that file, `grep -c '{#'` returned 0, and `grep -ci 'header_attributes'` also returned 0. As a check that the file was searchable, `grep -ci 'heading'` returned 67.

The spec's ATX heading rule (lines 1043 to 1053) starts from an opening sequence of `#` signs, and the string of characters after it gets parsed as inline content. That definition has no id part, so braces typed after the heading words belong to the heading content.

In pandoc, the syntax comes from extensions. The filtered listing from `pandoc --list-extensions=markdown | grep -e header_attributes -e auto_identifiers -e '^.attributes'` included `+auto_identifiers` and `+header_attributes`. For the commonmark reader, `pandoc --list-extensions=commonmark | grep -e header_attributes -e auto_identifiers -e '^.attributes'` printed `-attributes` among its lines.

## Setting an id in braces with the markdown reader

One heading with braces, a link to it, and two more headings with the same words make up `hid.md`:

```markdown
# Setup steps {#setup}

See [setup](#setup).

# Setup steps

# Setup steps
```

Here are the first two lines that came back when `pandoc -f markdown -t html hid.md` ran:

```html
<h1 id="setup">Setup steps</h1>
<p>See <a href="#setup">setup</a>.</p>
```

The brace text left the heading, and the id came out as `setup`, the word after the `#` inside the braces. The link's `href="#setup"` matches that id. The two repeat headings below it also got ids from pandoc; the heading links guide linked above explains those.

## When the header attributes extension is off

This run took the extension away to show what the braces do without it: `pandoc -f markdown-header_attributes -t html hid.md`. The first two output lines:

```html
<h1 id="setup-steps-setup">Setup steps {#setup}</h1>
<p>See <a href="#setup">setup</a>.</p>
```

The heading text kept the braces. The heading still got an id, but that id was `setup-steps-setup`. The link kept `href="#setup"`, and no heading in this output held `id="setup"`.

A count backs that up. `pandoc -f markdown-header_attributes -t html hid.md | grep -c 'id="setup"'` gave 0. With the full `-f markdown` reader in its place, that count gave 1.

## Commonmark with and without the attributes extension

No plus or minus sign followed the reader name when `pandoc -f commonmark -t html hid.md` ran, and it printed:

```html
<h1>Setup steps {#setup}</h1>
<p>See <a href="#setup">setup</a>.</p>
<h1>Setup steps</h1>
<h1>Setup steps</h1>
```

None of the three headings got an id here. This run printed the braces as heading text, which fits the spec definition quoted earlier. The link still asked for `#setup`, yet no heading in this HTML carried that id.

Turning on pandoc's `attributes` extension changed the first heading. `pandoc -f commonmark+attributes -t html hid.md` returned:

```html
<h1 id="setup">Setup steps</h1>
<p>See <a href="#setup">setup</a>.</p>
<h1>Setup steps</h1>
<h1>Setup steps</h1>
```

The heading with braces got `id="setup"` and lost the brace text. The two plain repeat headings got no id in this run. In the `markdown` run they did get ids, so switching readers changed more than the line with braces.

## Adding a class next to the id

A separate test file, `hid-class.md`, holds one line with an id and a class:

```markdown
# Owner notes {#notes .wide}
```

Here is what came back from four reader settings:

- `pandoc -f markdown -t html hid-class.md` returned `<h1 class="wide" id="notes">Owner notes</h1>`.
- `pandoc -f commonmark+attributes -t html hid-class.md` returned that same line.
- `pandoc -f commonmark -t html hid-class.md` returned `<h1>Owner notes {#notes .wide}</h1>`.
- `pandoc -f markdown-header_attributes -t html hid-class.md` returned `<h1 id="owner-notes-notes-.wide">Owner notes {#notes .wide}</h1>`.

The dot marked `wide` as a class. In the braces the id came first, yet pandoc wrote `class` before `id` in its HTML. Counting shows the split: `pandoc -f markdown -t html hid-class.md | grep -c 'class="wide"'` gave 1, while running that pipe with `-f commonmark` gave 0.

## Linking to a custom id

In the `markdown` and `commonmark+attributes` runs, the link `[setup](#setup)` and the heading with `id="setup"` came out in the same HTML. In the other two runs, the link had no heading with a matching id.

For a link from one file to a heading that lives in another file, the path part is covered in the [local file links guide](markdown-links-to-local-files.md). A contents list built from heading links has its own [contents list guide](markdown-table-of-contents.md).

## Check a heading id with pandoc

When a heading link lands nowhere, test the file with whichever reader you intend to keep.

1. Show the file's text with `cat hid.md` and confirm the braces come after the heading words, on the heading line.
2. Run one conversion, for instance `pandoc -f commonmark -t html hid.md`. If `{#setup}` shows in the heading text, that reader made no id from the braces in this test.
3. Next, count the id. `pandoc -f markdown -t html hid.md | grep -c 'id="setup"'` gave `1`. With `-f commonmark` in place of `-f markdown`, the count was `0`, and `-f commonmark+attributes` gave `1`.
4. Compare with the extension off. Turn it off with `pandoc -f markdown-header_attributes -t html hid.md` and read the first `h1` line; here it held `id="setup-steps-setup"`.
5. Set the link beside the heading ids. All four runs in this test printed the link as `href="#setup"`, and a heading with `id="setup"` showed up in two of them.
6. For a class, count it too: `pandoc -f markdown -t html hid-class.md | grep -c 'class="wide"'` gave `1`, and with `-f commonmark` it gave `0`.

## Where Sashmark fits

In the v1 plan, Sashmark would have you edit text at the spot where the page renders it. Saving after such an edit would then bring the clean Round-Trip in that plan, leaving each byte of an unedited line as it was. Flagging what is new in a file since your last read is the planned job of its Changes View, meant to work with no git. It is in development and has not been released. The app itself has no AI features. See the [planned features](/features), the [help pages](/help) and the [roadmap](/roadmap) for where each part stands.

More at [sashmark.com](https://www.sashmark.com/).
