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 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.
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”.
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.
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:
scriptis removed, and with it every script on the page stops running.styleis removed, so the styling you wrote inline is gone.iframeis removed or restricted, which kills embedded videos and maps.- Event handler attributes such as
onclickare stripped from the tags that survive. classandidoften 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.
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.
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 want | Use |
|---|---|
| bold, italic, a link, a list | Markdown syntax |
| a table with merged cells | HTML |
| a note that can be collapsed | HTML details, or a component |
| a video or an embedded frame | HTML, and check the platform allows it |
| anything that must match the theme | a Markdown extension or a component |
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.
classNameis the canonical form for theclassattribute, andhtmlForforfor.
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.
| Written as | Actually renders as | Why |
|---|---|---|
| a block tag in the middle of a paragraph | the paragraph is cut in two | a block always starts its own block |
| HTML block, then text with no blank line | the text is swallowed by the block | the block ends at a blank line |
| a blank line inside an HTML block | the content after it is parsed again | the block ends there |
a script tag in the source | the tag is kept in the output | the parser is not a sanitiser |
a style block or an iframe | removed by the platform | the sanitiser has an allow-list |
| an HTML comment inside an MDX file | a compile error | MDX needs a JSX comment |
“Markdown cleans up dangerous HTML” is a dangerous belief. Write a script tag into a Markdown document and the parser hands it straight through — the tag you typed is in the generated HTML, character for character. The removal happens later, at the platform, and only if that platform has a sanitiser configured. So the safety of an embedded script depends on a setting you do not control, on a platform you may not have chosen, and it can change without the document changing at all. The rule that follows from this is simple: if Markdown syntax can express it, write Markdown, and if you must use HTML, check what the target platform does with it before you publish.
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.
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:
- Write the first sentence as an ordinary paragraph, with no markup at all.
- Put a blank line after it, then write the notice on a single line as a block of HTML, using a
divwith aclassattribute and the notice text as its only child. - 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.
- Write the last sentence as an ordinary paragraph and mark the button name with
**Submit**. - 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.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.
