Tutorials 18 min read

Markdown Throws Away Your Extra Spaces — What Survives an Indent, a Tab and a Blank Line, Measured in Six Renderers

MMNMNOTE
markdownwhitespaceindentationrenderingreferencecommonmark

Consider a simple address block, typed the way it should look:

    Jane Smith
    123 Elm Street
    Springfield

Paste that into a Markdown note and render it. The leading spaces vanish. All three lines run together into one flat paragraph, or — if you typed four spaces after a blank line — the block becomes a monospace code block. Neither is what you wanted.

Markdown does not preserve leading spaces, extra blank lines, or runs of spaces inside a line. Two separate layers remove them: the parser strips leading and trailing whitespace and promotes deeply indented lines into code blocks; the browser collapses any space run that survives. Here is what actually survives in six measured renderers — and the two methods that keep spacing reliably intact.1

The demand for a clear answer is persistent. Five Stack Overflow questions on Markdown whitespace have accumulated over 2.37 million combined views since 2010.2 The top question alone — "How to indent a few lines in Markdown markup?" — has been viewed 1,101,922 times and earned 679 votes.3 Existing answers pile up techniques (&nbsp;, &emsp;, <br>, Unicode space characters) with no account of which layer removes the space in the first place, or which technique turns a note into a code block. This reference card gives the full picture.


Why two layers eat your spaces

Markdown does not preserve spacing because it was never designed to. Two independent removal events happen in sequence: first the parser, then the browser.

John Gruber, who designed Markdown in 2004, stated the founding rule directly: "Normal paragraphs should not be indented with spaces or tabs."4 He also defined a blank line as part of a broader rule: "A blank line is any line that looks like a blank line — a line containing nothing but spaces or tabs is considered blank."5 Leading whitespace was off the table from the start.

The CommonMark 0.31.2 specification — the authoritative modern formalization of Markdown — makes the parser-level stripping explicit: "The paragraph's raw content is formed by concatenating the lines and removing initial and final spaces or tabs."6 Once the parser emits <p>word</p>, the leading space is gone from the HTML. No downstream step can restore it.

The second removal happens inside the browser. The W3C CSS Text Level 3 specification (a Candidate Recommendation Draft, not a final Recommendation) defines how browsers handle the space characters that do survive the parser: "Any collapsible space immediately following another collapsible space — even one outside the boundary of the inline containing that space, provided both spaces are within the same inline formatting context — is collapsed to have zero advance width."7 A run of spaces such as a b c, surviving the parser in five of the six renderers measured, is collapsed to a single space on screen by the browser.

These two stages are cumulative and independent. Your file still contains every character you typed. The renderer shows fewer of them.

flowchart TD
    A["Your .md file<br/>(spaces intact in the file)"]
    B["Markdown parser<br/>strips leading/trailing whitespace<br/>promotes 4-space/tab lines to code blocks"]
    C["HTML output<br/>(leading spaces gone;<br/>indented blocks as pre/code)"]
    D["Browser CSS engine<br/>collapses inner space runs<br/>(CSS Text 3 §4.1)"]
    E["Screen<br/>(single spaces only —<br/>or entities / fenced code)"]

    A --> B --> C --> D --> E

Figure 1: Two independent layers remove whitespace. The parser acts on the raw Markdown source; the CSS engine acts on the HTML the parser produced. HTML entities (&nbsp;, &emsp;) and fenced code blocks bypass both stages.


The reference table: intent × renderer × what survives

Measurements taken 2026-10-29 using six parsers: GitHub API (GFM mode), commonmark.js 0.31.2, markdown-it 15.0.3, micromark 4.0.3 + gfm, pandoc 3.6.4 (markdown, gfm, and commonmark reader modes), Python-Markdown 3.11.2. Browser display verified in Chromium 156.8

IntentInputParser outputBrowser displaySurvives?
Indent a line with 2 leading spaces word after blank line<p>word</p> — stripped in all 6No indentNo
Indent with a tab after a blank line\tword after blank line<pre><code>word</code></pre> in all 6Monospace code blockNo (becomes code)
Indent with 4+ spaces after a blank line word after blank line<pre><code>word</code></pre> in all 6Monospace code blockNo (becomes code)
4 spaces directly under a paragraph (no blank line)paragraph\n wordContinuation of the paragraph in all 6paragraph word — joined, no indentNo
Extra blank lines between paragraphsline\n\n\nlineTwo <p> elements in all 6 — extra blanks discardedOne paragraph gapNo
Run of spaces inside a linea b cHTML retains the run in 5/6; pandoc collapses in the HTML itself (3 reader modes)a b c in all 6 (browser collapses)No
&nbsp; entities as indent (4×)&nbsp;&nbsp;&nbsp;&nbsp;wordU+00A0 × 4 preserved in <p> in all 6Visible four-character indentYes
&emsp; entities as indent (2×)&emsp;&emsp;wordU+2003 × 2 preserved in <p> in all 6Visible em-space indentYes
&nbsp;-only paragraph (vertical gap)&nbsp; on its own line between paragraphs<p>&#160;</p> in all 6Visible vertical gapYes
Typed U+00A0 or U+2003 at paragraph startRaw Unicode non-breaking or em space charactersDeleted by commonmark.js + Python-Markdown; kept by 4/6 (GitHub, markdown-it, micromark, pandoc)Visible in 4/6, gone in 2/6Unreliable
Fenced code block```\na b\n```<pre><code>a b</code></pre> in 5/6; Python-Markdown requires the fenced_code extensionExact spacing in monospace fontYes (5/6; Python-Markdown needs extension)
<pre> raw HTML block<pre>\na b\n</pre>Preserved in 5/6; escaped to literal text in markdown-it default and micromark defaultExact spacing where raw HTML is enabledYes (where html mode is on)

The code-block trap

The indented code block rule is the most common source of surprise, and the spec is precise about the trigger condition.

CommonMark defines what makes an indented code block: "An indented chunk is a sequence of non-blank lines, each preceded by four or more spaces of indentation."9 The key constraint follows immediately: "An indented code block cannot interrupt a paragraph."10 A blank line must appear first. The same four-space indent therefore behaves differently depending on its position:

Tabs follow the same logic. CommonMark specifies: "Tabs in lines are not expanded to spaces. However, in contexts where whitespace helps to define block structure, tabs behave as if they were replaced by spaces with a tab stop of 4 characters."11 A tab at the start of a line, after a blank line, triggers a code block. A tab inside a paragraph line passes through the parser and is collapsed to a single space by the browser.

Pandoc adds one more distinction: its markdown and gfm readers collapse inner space runs in the HTML output itself, producing <p>a b c</p> for input a b c. GitHub, commonmark.js, markdown-it, micromark, and Python-Markdown all pass the run through to the HTML — the browser then collapses it. The visual result is identical, but the HTML differs.8


What reliably survives

Two methods pass in all six renderers across all measured cases.

HTML entities

&nbsp; (non-breaking space, U+00A0) and &emsp; (em space, U+2003) survive because they are HTML entities, not raw whitespace characters. The Markdown parser passes them through as opaque text; the browser renders them as real, non-collapsible width. A line opening with &nbsp;&nbsp;&nbsp;&nbsp;word produces a visible four-character indent in every renderer tested. A paragraph containing only &nbsp; produces a visible vertical gap — because the parser keeps it as <p>&#160;</p>, a real box the browser renders with height.

One critical distinction: typed Unicode whitespace characters are not the same as entities. A typed U+00A0 (non-breaking space) at the start of a paragraph is deleted by commonmark.js and Python-Markdown, which call .trim() or .lstrip() on paragraph content — functions that strip all Unicode whitespace, not just ASCII space and tab.8 An &nbsp; entity at the same position survives in all six renderers. Use the entity form.

Obsidian's documentation describes the same display behavior for its Reading view: "Multiple adjacent blank spaces … collapse into a single space when displayed in Reading view."12 Standard CSS collapse rules apply inside its preview pane, so the entity technique works there too.

Fenced code blocks

When spacing is the content — a poem's layout, an ASCII-art diagram, a verse that depends on alignment — a fenced code block is the correct container. Spaces inside a fenced block are preserved exactly in five of the six renderers measured; Python-Markdown requires the fenced_code extension (off by default). The trade-off is rendering in a monospace font. For prose that needs alignment, entities offer per-character control without the monospace constraint.

Pandoc's pipeline offers a third option specific to itself: "A backslash-escaped space is parsed as a nonbreaking space."13 A backslash followed by a space produces a non-breaking space in pandoc's output. That shorthand is not portable to the other five engines.

On accessibility. Spacing used as visual layout carries a documented accessibility cost. WCAG 2.2 Technique F33, an informative technique (not required to meet WCAG 2 Success Criteria), observes that "using white space characters to create multiple columns does not provide the information in a natural reading order."14 Technique F32 covers the failure of using whitespace inside words.15 A table or a fenced code block communicates structure to assistive technology; a string of &nbsp; characters carries no semantic meaning a screen reader can convey. For structured data, use a Markdown table. For code or alignment-critical text, use a fenced block.


Quick-reference: which method to reach for

GoalReliable methodWhat makes it work
Indent a paragraph line&nbsp;&nbsp;&nbsp;&nbsp;wordNon-breaking space entities survive the parser's whitespace stripping
Em-space indent&emsp;wordU+2003 passes through as a non-collapsible entity
Extra vertical gap between paragraphsA paragraph containing only &nbsp;Becomes <p>&#160;</p> — a visible box with height
Preserve exact spacing (poem, table, aligned code)Fenced code block<pre><code> bypasses both parser and browser collapse
Non-breaking space in a pandoc pipeline\ (backslash + space)Pandoc-specific; not portable to GitHub, commonmark.js, markdown-it, micromark, or Python-Markdown
Spacing in raw-HTML-enabled environments<pre>...</pre>Bypasses CSS collapse where raw HTML rendering is on — off by default in markdown-it and micromark

The file has not changed

Before closing: the file has not changed. Every space you typed is still in the .md source. The renderer drops them on screen — the parser trims the edges, promotes the deep indents to code blocks, and hands the rest to the browser, which collapses the runs. Your note is not modified; what you see is a rendering decision, not a storage decision.

That distinction matters when spacing is intentional — a poem, an address block, an aligned table — because the author's intent is preserved in the file. A renderer that strips presentation from content is following a design principle: Markdown is about structure, not layout. When layout is the content, the correct tool is a code block, which instructs the renderer to stop interpreting and to preserve exactly.

For the CSS layer, the contract is stable. The CSS Text Level 3 Candidate Recommendation Draft specifies "document white space characters: spaces (U+0020), tabs (U+0009), and segment breaks"7 as the elements subject to collapse. The behavior has been consistent since CSS 2.1 and will apply to the next renderer you encounter.

For hard line breaks specifically — the trailing-two-space and <br> rules — see Parsing Markdown Edge Cases That Break Editors, which covers that ground in detail.16 For the full Markdown syntax reference, see The Complete Markdown Cheat Sheet. For the opposite problem — an invisible non-breaking space character that accidentally breaks a heading — see The Space That Looks Right and Still Breaks Your Heading.17

The spaces were never lost from your file. The renderer chose not to show them.


Frequently Asked Questions

How do I indent a line in Markdown without creating a code block?

Use HTML entities: &nbsp;&nbsp;&nbsp;&nbsp;word produces a four-character indent using non-breaking spaces. The &emsp; entity gives a wider em-space indent. Both survive in all six renderers measured. Four regular spaces or a tab only create a code block when they appear after a blank line — directly under a paragraph line they are stripped and the text is joined. Entity indents bypass both cases.

Why does my indent turn into a code block?

A tab or four-or-more spaces at the start of a line after a blank line triggers an indented code block under CommonMark. The spec states: "An indented code block cannot interrupt a paragraph." The blank line is the trigger, not the spaces alone. Remove the blank line above the indented text to suppress the code block, or switch to a fenced code block if the intent is to display code.

How do I add extra blank lines in Markdown?

Extra blank lines between paragraphs are discarded — the parser collapses any number of consecutive blank lines into a single paragraph separator. To create a visible vertical gap, insert a paragraph containing only &nbsp; between the two paragraphs. This renders as a non-printing box with height in all six renderers measured.

Do typed Unicode space characters (U+00A0, U+2003) work as an indent?

Not reliably. A typed U+00A0 or U+2003 at the start of a paragraph is deleted by commonmark.js and Python-Markdown, which strip all Unicode whitespace from paragraph edges. The same characters survive in GitHub, markdown-it, micromark, and pandoc. Because the behavior differs across engines, typed Unicode spaces are not portable. Use the &nbsp; or &emsp; entity form instead.

How do I preserve exact spacing — like a poem or an ASCII table?

Use a fenced code block. Spaces inside a fenced block are preserved exactly in five of the six renderers tested; Python-Markdown requires the fenced_code extension. The trade-off is rendering in a monospace font. For prose-style spacing where monospace is unwanted, &nbsp; and &emsp; entities give per-character control.

Does a tab inside a line behave differently from a tab at the start?

Yes. A tab at the start of a line after a blank line triggers a code block (equivalent to four spaces under CommonMark's tab-stop rule). A tab inside a line — between two words — passes through the parser and is collapsed to a single space by the browser. The same visual result as a single space, via the CSS Text Level 3 Candidate Recommendation Draft collapse rule.

Why does CSS Text 3 say "Candidate Recommendation Draft" instead of "Recommendation"?

CSS Text Level 3 is currently at the W3C Candidate Recommendation Draft stage — mature and under review, not yet a finalized W3C Recommendation. The white-space collapsing behavior it specifies has been implemented uniformly across browsers since CSS 2.1 and is not in flux. The "Draft" label describes the specification's editorial status, not the stability of the underlying behavior.


References


Notes live the way you wrote them — every space intact in the file. MNMNOTE keeps your Markdown on your own device, exactly as you typed it. mnmnote.com


Footnotes

  1. CommonMark 0.31.2 §4.8 Paragraphs. https://spec.commonmark.org/0.31.2/#paragraphs ↩

  2. Stack Overflow API, measured 2026-10-29 via api.stackexchange.com/2.3/questions, tagged=markdown. Five questions: q/6046263, q/20543454, q/44810511, q/40023013, q/15721373. Total view count: 2,372,427. ↩

  3. Stack Overflow q/6046263 "How to indent a few lines in Markdown markup?" — 1,101,922 views / 679 votes, measured 2026-10-29. https://stackoverflow.com/questions/6046263 ↩

  4. John Gruber. "Markdown: Syntax — Paragraphs." Daring Fireball, 2004. https://daringfireball.net/projects/markdown/syntax#p ↩

  5. John Gruber. "Markdown: Syntax — Paragraphs." Daring Fireball, 2004. https://daringfireball.net/projects/markdown/syntax#p ↩

  6. CommonMark 0.31.2 §4.8 Paragraphs. https://spec.commonmark.org/0.31.2/#paragraphs ↩

  7. W3C CSS Text Level 3 §4.1 White Space Processing (Candidate Recommendation Draft, 14 August 2026). https://www.w3.org/TR/2026/CRD-css-text-3-20260814/#white-space-phase-1 ↩ ↩2

  8. Own measurement, receipts/tally.txt, 2026-10-29T01:19Z. Parsers: GitHub API (GFM), commonmark.js 0.31.2, markdown-it 15.0.3, micromark 4.0.3 + gfm, pandoc 3.6.4 (markdown/gfm/commonmark reader modes), Python-Markdown 3.11.2. Display: Chromium 156. ↩ ↩2 ↩3

  9. CommonMark 0.31.2 §4.4 Indented code blocks. https://spec.commonmark.org/0.31.2/#indented-code-blocks ↩

  10. CommonMark 0.31.2 §4.4 Indented code blocks. https://spec.commonmark.org/0.31.2/#indented-code-blocks ↩

  11. CommonMark 0.31.2 §2.2 Tabs. https://spec.commonmark.org/0.31.2/#tabs ↩

  12. Obsidian Help. "Basic formatting syntax," line 30. https://raw.githubusercontent.com/obsidianmd/obsidian-help/master/en/Editing%20and%20formatting/Basic%20formatting%20syntax.md ↩

  13. pandoc User's Guide. "Backslash escapes." https://pandoc.org/MANUAL.html#backslash-escapes ↩

  14. W3C WAI WCAG 2.2 Technique F33 (informative — not required to meet WCAG 2 Success Criteria). https://www.w3.org/WAI/WCAG22/Techniques/failures/F33 ↩

  15. W3C WAI WCAG 2.2 Technique F32 (informative — not required to meet WCAG 2 Success Criteria). https://www.w3.org/WAI/WCAG22/Techniques/failures/F32 ↩

  16. MNMNOTE Blog. "Parsing Markdown Edge Cases That Break Editors." https://blog.mnmnote.com/parsing-markdown-edge-cases-that-break-editors ↩

  17. MNMNOTE Blog. "The Space That Looks Right and Still Breaks Your Heading." https://blog.mnmnote.com/the-space-that-looks-right-and-still-breaks-your-heading ↩