Skip to content

MD003 - Heading style should be consistent

Aliases: heading-style

What this rule does

Ensures all headings in your document use the same formatting style.

Why this matters

  • Readability: Consistent heading styles make documents easier to scan and navigate
  • Professionalism: Mixed heading styles look unprofessional and unpolished
  • Tool compatibility: Some tools expect consistent heading formats for navigation features

Examples

✅ Correct (using # style)

# First Level Heading
## Second Level Heading
### Third Level Heading

✅ Correct (using underline style)

First Level Heading
==================

Second Level Heading
------------------

✅ Correct (using setext-with-atx style)

First Level Heading
==================

Second Level Heading
------------------

### Third Level Heading
#### Fourth Level Heading

✅ Correct (using setext-with-atx-closed style)

First Level Heading
==================

Second Level Heading
------------------

### Third Level Heading ###
#### Fourth Level Heading ####

❌ Incorrect (mixed styles)

# First Level Heading

Second Level Heading
------------------

### Third Level Heading

🔧 Fixed

# First Level Heading
## Second Level Heading
### Third Level Heading

Configuration

[MD003]
style = "consistent"  # Options: "consistent", "atx", "atx-closed", "setext", "setext-with-atx", "setext-with-atx-closed"

Style options explained

  • "consistent" (default): Use the most prevalent style in your document (in case of a tie, ATX style is preferred as it's most widely supported)
  • "atx": Use # symbols (# Heading)
  • "atx-closed": Use # symbols at both ends (# Heading #)
  • "setext": Use underlines (equals for level 1, dashes for level 2)
  • "setext-with-atx": Use underlines for level 1-2, # symbols for level 3-6
  • "setext-with-atx-closed": Use underlines for level 1-2, # symbols with closing # for level 3-6

Note: Underline style only works for level 1 and 2 headings. Level 3 and below must use # symbols.

Automatic fixes

This rule can automatically convert all headings to match your configured style or the most prevalent style in the document.

Headings inside blockquotes

Headings inside blockquotes are checked like any other heading, including headings in nested quotes, in a quote inside a list item, and in a list item inside a quote. They are held to the configured style, and under style = "consistent" they count toward the prevalent style exactly as unquoted headings do (a heading inside a list item is counted the same way). A quoted heading can therefore decide the document style, and a document whose only headings are quoted is checked too.

# Title

> Quoted heading
> --------------

Under consistent (or atx) the quoted setext heading above is reported: Heading style should be # Heading, found Heading (underlined with -).

Quoted headings are reported without an automatic fix. Rewriting one has to rebuild the quote prefix on every line it touches, and a setext underline needs the same prefix as its text, so rumdl check --fix leaves quoted headings as written and the warning remains until you change them by hand. Unquoted headings in the same document are still fixed.

Markdown with Gherkin

Under the mdg flavor, MD003 steers every heading to plain ATX whatever style is configured. Markdown with Gherkin parses ATX headings only — one to six # characters followed by a space — so a Setext heading never becomes a Gherkin node, and a closing sequence leaks into the node's name, making # Feature: F # a node named F #.

When a fixed style other than atx was configured explicitly and the override applies in MDG, rumdl prints one [config warning] per process. The default consistent style and an explicit atx do not warn.

❌ Incorrect

Feature: Checkout
=================

## Scenario: Purchase ##

✅ Correct

# Feature: Checkout

## Scenario: Purchase

Under any other flavor the configured style is applied as usual.

Limitation

Converting a Setext heading changes its source text, not just its Markdown presentation. In an MDG document without an explicit # Feature: heading, Cucumber may derive the feature name from the leading Markdown content; adding the ATX marker can therefore add a literal # and following space to that parsed feature name. This occurs in Cucumber's testdata/good/misc.feature.md. Avoid Setext headings in MDG, or disable MD003 when that AST must remain unchanged.

See Markdown with Gherkin Flavor for the full flavor specification.

Learn more

  • MD001 - Heading levels should only increment by one
  • MD022 - Headings should be surrounded by blank lines
  • MD023 - Headings must start at the beginning of the line