A practical guide to enabling HTML in Markdown without opening XSS vulnerabilities.
Allowing buttons, dialogs, and UI components with react-markdown
Markdown is excellent for documentation. It is simple, readable, and widely supported.
But Markdown alone can be limiting when you want to demonstrate real UI elements such as
- buttons
- dialogs
- popovers
- form inputs
If you try to add HTML to Markdown using react-markdown, you may notice something surprising: the HTML often does not render at all.
This article explains:
- why HTML is disabled in Markdown renderers
- how to safely enable HTML rendering
- how to create a secure allow-list schema
- how this approach is used in the CSSEXY UI editor to render Markdown documentation with interactive examples.
Why HTML doesn't render in react-markdown
A typical Markdown renderer in React looks like this:
import Markdown from "react-markdown";
import remarkGfm from "remark-gfm";
...
<Markdown remarkPlugins={[remarkGfm]}>
{markdownText}
</Markdown>remark-gfm enables GitHub Flavoured Markdown, including:
- tables
- task lists
- strikethrough
- autolinks
Example Markdown:
## Example
- item one
- item twoThis renders perfectly.
But if you try this:
<button>Click me</button>the button does not appear as a button. It will usually render as plain text.
This behavior is intentional.
Why HTML is disabled by default
Allowing arbitrary HTML inside Markdown can introduce cross-site scripting (XSS) vulnerabilities.
For example:
<script>alert("XSS")</script>or
<img src="x" onerror="stealCookies()">If your application rendered this HTML, malicious JavaScript could execute in the user's browser.
Because of this risk, react-markdown disables raw HTML by default.
A real example: the CSSEXY editor
In the CSSexy visual UI editor Markdown is used to write help text and documentation directly inside the application.
Writing text as Markdown in CSSexy has the advantage that you don't have to add header, paragraph, list item … as a node in the HTML tree, just use Markdown syntax like #, double linebreak, … in the text, and CSSexy generated the respective HTML elements for you.
Sometimes these help texts need to include real UI elements — for example buttons, dialogs or popovers — so users can see exactly how a component behaves.
This article describes the approach used in the CSSEXY editor to render Markdown documentation with safe interactive HTML examples.
How to render HTML in react-markdown safely
To allow HTML inside Markdown, we need two additional plugins:
- rehype-raw → parses raw HTML
- rehype-sanitize → removes unsafe elements
Install them:
yarn add rehype-raw rehype-sanitizeThen configure react-markdown:
import rehypeRaw from "rehype-raw";
import rehypeSanitize from "rehype-sanitize";
<Markdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw, rehypeSanitize]}
>
{markdownText}
</Markdown>The rendering pipeline now works like this:
Markdown
↓
react-markdown
↓
rehype-raw
↓
rehype-sanitize
↓
Safe HTML rendered in ReactHowever, one more step is required.
Why your HTML disappears when using react-markdown
A common problem when enabling HTML in Markdown is that elements still do not appear.
Example Markdown:
<button>Click me</button>Expected result: a button.
Actual result:
Click meThis usually happens for one of three reasons.
1. HTML parsing is not enabled
react-markdown does not parse HTML by default.
You must include rehype-raw:
rehypePlugins={[rehypeRaw]}2. The sanitizer removes the element
Even when HTML parsing works, rehype-sanitize removes elements that are not allowed.
For example <button> is not part of the default schema.
So the sanitizer removes it.
3. Attributes are stripped
Sometimes the element appears but attributes disappear.
Example Markdown:
<button class="button">Click</button>If class is not allowed in the schema, it will be removed.
This means the button renders but without styling.
Creating a safe HTML allow-list
The correct solution is to extend the sanitization schema and explicitly allow the elements you need.
Example:
import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
const markdownSchema = {
...defaultSchema,
tagNames: [
...(defaultSchema.tagNames || []),
"button",
"dialog",
"div",
"span",
"section",
"label",
"input"
],
attributes: {
...defaultSchema.attributes,
"*": [
...(defaultSchema.attributes?.["*"] || []),
"id",
"class",
"className",
"title",
"role",
"tabindex",
/^data-[\\w-]+$/,
"aria-label",
"aria-labelledby",
"aria-describedby",
"aria-controls",
"aria-expanded"
],
button: [
"type",
"disabled",
"name",
"value",
"popovertarget",
"popovertargetaction"
],
dialog: [
"open"
]
}
};Then use the schema in the Markdown renderer
<Markdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw, [rehypeSanitize, markdownSchema]]}
>
{markdownText}
</Markdown>This approach allows only specific elements and attributes.
Everything else is automatically removed.
Example: rendering a button
Markdown input:
## Example button
<button class="button md blue-outline">
Click me
</button>A real interactive button rendered inside the documentation.
To actually see the result go to:
In the CSSexy Editor, HTML tab,
- add a
divand atextnode under thediv. - Open the Edit tab for the
textnode. - Check
markdownunderneath the input area. - Write Markdown text within the input area.
What should never be allowed
Even with sanitization, some HTML elements should always remain blocked:
script
style
iframe
foreignObject
on*These elements can execute scripts or manipulate the page.
A safe schema should never allow them.
Why this architecture works well
This setup provides a good balance between flexibility and security.
Advantages:
- Markdown remains easy to write
- documentation can include real UI elements
- interactive examples are possible
- security risks remain controlled
Instead of allowing arbitrary HTML, you maintain a clear allow-list of elements and attributes.
Conclusion
Rendering HTML inside Markdown can make documentation significantly more powerful — especially for UI-focused platforms.
Using:
react-markdownrehype-rawrehype-sanitize
with a custom allow-list schema allows you to embed real UI elements like buttons, dialogs and popovers safely.
For tools like CSSEXY, where documentation and UI examples live side-by-side, this turns Markdown into a powerful interactive documentation system.