Skip to main content
Markdown

Markdown vs HTML for docs: READMEs, API reference, and where each one breaks

Markdown wins anything that lives next to code and gets diffed. HTML wins when you need real layout. The GFM spec rules that decide which one you're actually using.

Thien Nguyen
By Thien Nguyen
Updated August 2, 2026 · 8 min read

Use Markdown for anything that lives next to code and gets diffed — READMEs, API reference, contributing guides, ADRs. Reach for HTML, or a docs-site generator that emits it, when you need real page layout, interactive components, or branding. A README was never meant to be a marketing page, and a marketing page has no business being reviewed in a pull request. The interesting part isn't the choice; it's that Markdown quietly hands you an HTML escape hatch, and that escape hatch has rules most people learn by shipping a broken README.

Markdown's actual advantage is that it's legible unrendered

The pitch usually given for Markdown is "it's easier to write." That's true and it's not the point. The point is that Markdown is readable as source, in the places where nothing renders it:

  • cat README.md over SSH on a box with no browser
  • a git diff in a terminal during review
  • your editor's split-second glance before the preview pane catches up
  • a merge conflict, where you're reading raw text under pressure

Put the same content in HTML and every one of those degrades. A one-word copy fix in an HTML doc shows up in review as a diff line wrapped in <p> tags and whatever indentation the formatter felt like; the same fix in Markdown is a one-word diff. That's not aesthetics, it's review throughput — and reviewers who can read the diff actually read the diff.

The second advantage is portability, and it's the one people underrate until they need it. One README.md renders on GitHub, GitLab, npm, PyPI, Bitbucket, your editor's preview, and half a dozen static-site generators, with no build step anywhere in that list. HTML renders in a browser. That's it. If your docs are meant to show up in more than one place you don't control, the format that needs zero toolchain wins by default.

Where Markdown runs out — and hands you HTML

Markdown has no layout primitives. No columns, no boxes, no alignment, no width control. This is deliberate, and it's why every serious flavor ships an escape hatch: raw HTML is legal inside Markdown. GFM inherits that from CommonMark, which is explicit about the mechanism — "the simple and flexible way of including Markdown content inside HTML tags: simply separate the Markdown from the HTML using blank lines."

That sentence is doing a lot of work, and getting it wrong is the single most common README bug I see.

The worked example: a warning box, three ways

Say you want a centered warning callout in a README. Here's the version almost everyone writes first:

<div align="center">
  <strong>Warning:</strong> this deletes the `data/` directory. See the [backup guide](./BACKUP.md) first.
</div>

This renders wrong. Not "looks slightly off" — the backticks and the link ship to the reader as literal punctuation. Under CommonMark's HTML block rules, an opening <div> on its own line starts a type-6 HTML block, and that block only ends at a blank line. Everything until then is passed through as raw HTML with no Markdown parsing. The spec's own minimal case:

<div>
*Emphasized* text.
</div>

comes out the other side as literally <div> newline *Emphasized* text. — the asterisks intact, no <em> anywhere. Add blank lines and the same input parses completely differently:

<div>

*Emphasized* text.

</div>

now produces <div> wrapping <p><em>Emphasized</em> text.</p>. Same characters, two blank lines apart, opposite results.

So the callout, fixed — blank lines let the Markdown inside stay Markdown:

<div align="center">

**Warning:** this deletes the `data/` directory. See the [backup guide](./BACKUP.md) first.

</div>

And the all-HTML version, for comparison:

<div align="center">
  <p><strong>Warning:</strong> this deletes the <code>data/</code> directory.
  See the <a href="./BACKUP.md">backup guide</a> first.</p>
</div>

Both render the same. The middle one stays greppable, stays diffable as prose, and survives someone editing the link text without knowing HTML. The takeaway: use the minimum HTML that buys you the layout, keep the content in Markdown, and remember the blank lines — they're what makes the hybrid work at all.

Morgan Freeman applauding — two blank lines, and the callout finally renders

The mental model that keeps you out of trouble: a raw HTML tag in Markdown is a container, not a mode. Blank lines close the container and give you Markdown back. If you find yourself writing <strong> instead of **, you've stopped using the escape hatch and started rewriting your doc in HTML by accident.

The comparison, honestly

MarkdownHTML
Readable as sourceYes — that's the entire design goalNo; content is buried in tags
PortabilityGitHub, GitLab, npm, PyPI, editors, SSGs — same fileBrowser only, or a converter
Layout controlEffectively none without embedded HTMLTotal
Build stepNone requiredNone for a single file; a real docs site needs one
Diff-friendlinessOne-word change is a one-word diffReformatting churns lines that didn't change
Sanitization riskRenderer-dependent, but little to stripRegistries strip most of what you'd style with

That last row is where the "just use HTML for control" plan dies quietly. GFM's spec has an entire extension, §6.11 Disallowed Raw HTML, that filters nine tags out of the output: title, textarea, style, xmp, iframe, noembed, noframes, script, plaintext. The filtering is blunt — it replaces the leading < with &lt;, so your <style> block doesn't get dropped, it gets displayed as text in the middle of your README.

Worth keeping the layers straight, because people conflate them: that tag filter is the spec. Separately, GitHub.com applies its own post-processing — the GFM spec notes that "GitHub.com and GitHub Enterprise perform additional post-processing and sanitization after GFM is converted to HTML" — and that's the layer that strips presentational attributes. Between the two, "full design control via raw HTML in a README" isn't a tradeoff you're making. It's a thing that does not exist.

The parts of Markdown that are genuinely annoying

Being fair to HTML means admitting where Markdown is bad, and it's bad in specific, reproducible ways.

Tables are the worst part. GFM tables can't hold block content at all — the spec says flatly that "block-level elements cannot be inserted in a table." No code fences in a cell, no lists, no paragraphs. And the failure mode is silent: per the spec, if a body row has more cells than the header row, "the excess is ignored." Type one stray | inside a cell without escaping it and you've split that cell in two, pushed a column off the end, and lost it — no error, no warning, just a table that's quietly missing data. Same shape as the classic YAML NO-parses-as-false bug: the parser did exactly what it promised, and nobody noticed. Escape pipes as \|, including inside inline code spans.

Nested lists are finicky for a reason nobody remembers. Continuation content has to be indented to the content column of the parent item, which depends on the marker's width. Under - that's two spaces; under 1. it's three. Guess wrong and your sub-list becomes a code block or flattens into the parent paragraph, differently on different renderers.

Portability has a ceiling, and extensions are where you hit it. GitHub's alert syntax is the current favorite:

> [!WARNING]
> This deletes the data directory.

Lovely on GitHub. But it isn't in the GFM spec — it's a GitHub product feature, not a spec extension — so any renderer that doesn't implement it falls back to a plain blockquote containing the literal text [!WARNING]. Which is fine, actually: it degrades to something readable. That's the test worth applying to every Markdown extension you adopt. If it degrades to readable text elsewhere, use it. If it degrades to garbage, you've made your README worse everywhere except one website.

So when does HTML actually win?

Three cases, and they're narrower than people assume:

  1. A public docs site with real design. Navigation, search, versioned sidebars, a landing page with brand typography. But note what you're really choosing here — almost nobody hand-writes this. You write Markdown or MDX and a generator emits the HTML. So the honest framing isn't "HTML instead of Markdown," it's "Markdown plus a build step, with HTML as the artifact."
  2. Genuine interactivity. A runnable API console, a parameter playground, a live diagram. Markdown has no answer and the escape hatch won't save you, because <script> is one of the nine tags the tagfilter escapes.
  3. Email and other hostile renderers. Release-note emails, in-app help panes — anywhere the target isn't a Markdown renderer at all.

Everything else — README, CONTRIBUTING, API reference, changelogs, ADRs, runbooks — is Markdown, because all of it is versioned, reviewed, and read as often in a diff as in a browser.

The migration direction tells you something too. Teams move HTML docs into Markdown constantly, usually when docs drift because only one person can edit them; a converter like html-to-markdown gets you 90% of the way and you hand-fix the layout divs. Almost nobody goes the other way permanently — when you need HTML you generate it from Markdown at build time, or with markdown-to-html for a one-off, and the Markdown stays the source of truth.

Which is the actual answer to "Markdown or HTML for docs." It's rarely either. It's Markdown as the thing humans write and review, HTML as a rendering target, and a small, deliberate amount of raw HTML in between — with blank lines around it.

Cover photo by Bibek ghosh on Pexels.

References

Primary documentation and specifications checked when this article was last updated.

MarkdownDocumentationDeveloper Tools

Related articles

All articles