Embedding HTML

Markdown allows you to write HTML directly in the document. That single feature is the source of most of its flexibility — you can drop in a table with merged cells, an image with a caption, a collapsible panel, an embedded video — and it is also the source of most of its unpredictability. The same source file, rendered by two different platforms, can produce two different pages: one platform passes the markup through, the other silently removes it.

The reason is that there are two separate decisions here, and people routinely conflate them. The first is the parser’s decision: what does this renderer do with the markup when it reads the file? The second is the platform’s decision: what does this site allow in the final HTML it serves? A Markdown parser is not a security layer, so the first decision almost always ends in “the markup is kept”. The second decision is made somewhere else entirely, by whoever runs the site.

This page walks through the rules the parser actually follows — where an HTML block may sit, where it ends, and what it does to the paragraph next to it — then turns to the platform side: what gets stripped, what stops matching the theme, and when a Markdown extension is the better answer. It closes with the differences you meet once the file itself is MDX rather than plain Markdown.

Inline HTML and Block-Level HTML

Inline HTML sits inside a paragraph, mixed in with the words:

A paragraph with <span class="x">inline markup</span> inside.
<p>A paragraph with <span class="x">inline markup</span> inside.</p>

The tag is emitted into the output exactly as written, sitting between two runs of text. Nothing about the surrounding sentence changed, and the Markdown inside the same paragraph — emphasis, links, code — is still parsed as usual.

Block-level HTML stands on its own line. The usual suspects are div, table, details, figure:

This is a paragraph
<div class="note">This is a custom HTML block</div>
<p>This is a paragraph</p>
<div class="note">This is a custom HTML block</div>

Look closely at the output, because the difference from the inline case is the important part. The sentence “This is a paragraph” could have continued onto the next line, and in a plain Markdown document it would have. But a block-level tag always starts its own block: the paragraph is closed, and the div becomes a sibling of it rather than a continuation.

Where an HTML Block Ends

This is the most counterintuitive rule in the whole area, and it is the one that produces most of the broken pages. An HTML block ends at a blank line. Everything up to that blank line is HTML; everything after it is Markdown again, parsed normally:

<div>

**Bold** is parsed

</div>
<div>
<p><strong>Bold</strong> is parsed</p>
</div>

The bold text really is parsed, and it really ends up inside the div. The idea that “Markdown inside an HTML block is not parsed” is not quite right — the parser simply has to reach the blank line before it can tell that the block is over, and by then the Markdown has been parsed as usual.

The other direction is the one that silently loses content. Without a blank line there is no end, so the line after the block is swallowed by it and never becomes a paragraph of its own:

<div>Block</div>
The paragraph that follows
<div>Block</div>
The paragraph that follows

The second line is there in the output, but it is not a paragraph — it is raw text sitting inside the HTML block. It has no <p> wrapper, so it inherits the block’s styling, and any Markdown in it would be left as literal text. So whenever you add a new line after a block of HTML, put a blank line in between. That single blank line is the whole difference between “a new paragraph” and “more raw text inside the div”.

The Parser Does Not Filter for You

A Markdown parser is a text processor. It does not know what a script tag is, and it does not have a list of dangerous tags, because it has no reason to keep one:

<script>alert(1)</script>
<script>alert(1)</script>

The tag is in the output, character for character. If you were expecting the Markdown side to clean this up, that expectation is the problem: the filtering, when it happens, happens further down the pipeline.

What a Publishing Platform May Remove

That further-down-the-pipeline stage is the sanitiser, and it belongs to the platform: the static site generator, the comment host, the wiki, the rendering service. A typical sanitiser has a short allow-list of elements and attributes, and everything else is dropped or escaped. In practice:

  • script is removed, and with it every script on the page stops running.
  • style is removed, so the styling you wrote inline is gone.
  • iframe is removed or restricted, which kills embedded videos and maps.
  • Event handler attributes such as onclick are stripped from the tags that survive.
  • class and id often survive but carry no meaning, because the stylesheet that would use them belongs to the platform and knows nothing about your names.

The practical consequence is that the same document can look right on your own machine and fall apart after you publish it. Nothing in the Markdown source tells you which will happen — you have to know the target platform, or check it after publishing.

Styles Do Not Follow the Theme

Even when the markup survives, the look rarely does. A page is themed by the platform’s stylesheet: its font stack, its colour palette for light and dark mode, its spacing scale, its heading sizes. Your inline HTML arrives with none of that context. A div with a hard-coded background colour will stay that colour when the site switches to dark mode, and a table with no class will have none of the borders, padding or zebra striping that every other table on the site has.

The fix is never to hard-code the values. Use the platform’s own mechanism — a utility class the design system provides, or a component the site exposes — and let the theme do its job. If the platform offers no such mechanism, that is a strong signal that the thing you are building does not belong in raw HTML at all.

HTML or a Markdown Extension?

This is the decision to make deliberately rather than by habit, because the two options have very different costs. Markdown syntax and extensions are portable: they render the same way in every tool that supports them, they inherit the theme automatically, and when something goes wrong the cause is usually visible in the source. Raw HTML is none of those things — it is a dependency on one renderer, one theme, and one sanitiser.

So reach for HTML when Markdown genuinely cannot express what you need: an image with a title attribute, a collapsible details block, a video element, an embedded frame, or a layout that has to be built from table cells. Reach for a Markdown extension when the platform offers one — footnotes, task lists, tables, strikethrough — because those are designed to look native. And reach for neither when a plain sentence will do.

What you wantUse
bold, italic, a link, a listMarkdown syntax
a table with merged cellsHTML
a note that can be collapsedHTML details, or a component
a video or an embedded frameHTML, and check the platform allows it
anything that must match the themea Markdown extension or a component

The Same Markup in an MDX File

The pages of this course are MDX, and MDX parses embedded HTML as JSX rather than as raw text. Three consequences matter in practice:

  • Every tag must be closed. A void element has to be self-closed, so you write <br /> and not <br>. An unclosed tag is a compile error, not a rendering quirk.
  • HTML comments are not allowed. The <!-- ... --> form is a syntax error in MDX, because it is not valid JSX. The MDX equivalent is a JSX comment, written as {/* ... */}.
  • Attribute names follow JSX spelling. className is the canonical form for the class attribute, and htmlFor for for.

A brace is also special in MDX: it opens an expression, so a lone { in a paragraph is an error rather than a character. These are the differences that make an MDX file stricter than a Markdown file, and they are worth knowing before you paste a snippet you found in a plain Markdown document.

Common Mistakes

Written asActually renders asWhy
a block tag in the middle of a paragraphthe paragraph is cut in twoa block always starts its own block
HTML block, then text with no blank linethe text is swallowed by the blockthe block ends at a blank line
a blank line inside an HTML blockthe content after it is parsed againthe block ends there
a script tag in the sourcethe tag is kept in the outputthe parser is not a sanitiser
a style block or an iframeremoved by the platformthe sanitiser has an allow-list
an HTML comment inside an MDX filea compile errorMDX needs a JSX comment

Exercises

Rewrite the following passage as Markdown. Requirements: the first sentence must be an ordinary paragraph; a one-line block of HTML must introduce the notice; a blank line must follow that block; and the bold word in the last sentence must still render as bold.

This paragraph is ordinary text.

Notice: please verify your identity first.

The last step is to press the Submit button.
Block-level HTMLBlank-line boundaryInline HTML

Approach:

The notice is written as a one-line block of HTML, and the decisive detail is the blank line after it. A block-level tag ends at a blank line; without one, the following sentence is treated as more raw content inside the block, so it never becomes a paragraph and the bold word inside it never renders as bold.

Steps:

  1. Write the first sentence as an ordinary paragraph, with no markup at all.
  2. Put a blank line after it, then write the notice on a single line as a block of HTML, using a div with a class attribute and the notice text as its only child.
  3. Put a blank line immediately after that line. This blank line is what ends the HTML block and lets the next sentence start a new paragraph.
  4. Write the last sentence as an ordinary paragraph and mark the button name with **Submit**.
  5. Check the result: three separate blocks appear — the paragraph, the notice, and the closing sentence — and the bold in the last one is rendered as bold.

Answer:

This paragraph is ordinary text.

<div class="note">Notice: please verify your identity first.</div>

The last step is to press the **Submit** button.

Summary

You can now tell inline HTML from block-level HTML, and you know that a block-level tag interrupts the paragraph it appears in. You know that an HTML block ends at a blank line and keeps swallowing lines until it finds one, which is the first thing to check when a page loses a paragraph. You know that the parser keeps what you write, that the platform decides what survives, and that an inline style or a script tag will not survive everywhere. You can also say when HTML is the right tool, when a Markdown extension is, and how an MDX file differs by requiring closed tags and JSX comments. With that settled, the next page moves to the syntax that carries real meaning in a technical document: mathematics.

Found an issue with this content?

Tell us what could be improved.