Markdown to HTML Converter_

Convert Markdown to HTML, with the dialect stated rather than guessed. This page renders GitHub Flavored Markdown by default — tables, task lists, strikethrough and autolinks all work — and CommonMark is one control away when the destination is stricter. The HTML is sanitised before you ever see it, and the preview beside it is the same string you copy.

Free, no account, and the document never leaves the page.

toolkit.codes/markdown-to-html
Nothing to convert yet
UTF-8
Ready
100% LOCAL

Markdown has no single definition, and a converter picks one for you

There is no such thing as "the" Markdown syntax. John Gruber's 2004 original had no formal grammar and left dozens of cases undefined; CommonMark was written years later to pin those down; GitHub then built its own layer on top of CommonMark for the things a code host needs. Those are three different languages with the same file extension, and a converter has to choose between them before it can produce a single tag.

Almost none of them tells you which it chose. That is why the most common complaint about this category of tool is some version of "my table did not render" — the document was written for GitHub and the converter implemented CommonMark, where a table is a paragraph containing pipe characters. The answer is not a better parser. It is knowing which dialect is running, and being able to change it.

The four constructs that decide which dialect you need

GitHub Flavored Markdown is a strict superset of CommonMark: everything CommonMark defines works identically, and four things are added. Tables. Strikethrough with ~~tildes~~. Task lists, the - [ ] checkboxes. And autolinks, where a bare URL in the text becomes a link without being wrapped in anything. If your document contains none of those four, both dialects give you byte-identical output and the control does not matter. If it contains any of them, that control is the whole conversion.

The panel says so rather than leaving you to spot it: pick CommonMark on a document using GitHub syntax and it names exactly which constructs are being ignored.

Raw HTML in Markdown is a security question, not a convenience

Every Markdown dialect permits raw HTML in the source, and every parser passes it straight to the output untouched. That is specified behaviour, not a bug — it is how you get a <details> block or a sized image into a README. It also means a Markdown document is an arbitrary HTML document wearing a disguise, and a <script> tag in it becomes a <script> tag in the result.

That is fine when you wrote the document. It is not fine for a pull request body, an issue someone filed, a comment from a form, or a file you were sent — and those are exactly the documents people convert. So the HTML here is sanitised before it reaches you, and the policy is written down in the technical details below rather than implied. Anything removed is named in the panel, because deleting someone's markup silently teaches them nothing except that the tool is broken.

Why the preview is in a sandbox

A preview means running a stranger's HTML inside a page. Sanitising it first is necessary and it is not sufficient, because it makes one filter the only thing between a hostile document and your browser. So the preview renders inside a frame with the sandbox attribute and nothing enabled in it: no scripts, no access to this page, no form submission, no navigation. Nothing in a previewed document can run even if the sanitiser were wrong about it.

The two layers are not redundant, they cover different ground. The frame protects this page. The sanitiser protects the site you paste the output into, which has no frame around it. And the frame is fed the exact string the Copy button gives you — a preview showing anything else would be a lie in the one place a converter exists to be honest.

A fragment, or a file you can open

Fragment is the default: the markup for your content and nothing else, which is what you want when it is going into a template, a CMS field or a component. Document mode wraps it as a real file — doctype, language, charset, viewport, and a <title> taken from your first heading — so downloading it and opening it in a browser works. No stylesheet is included in either mode, deliberately. Converter output that arrives carrying somebody else's CSS is output you have to strip before you can use it.

In Python, at the command line, or in your own build

For a one-off, pandoc file.md -o file.html is the shortest route, and adding -s gives you a standalone document rather than a fragment — the same distinction this page's Output control makes. In Python, markdown.markdown(text) from the markdown package handles the basics and needs its tables and fenced_code extensions switched on to reach GitHub parity. In JavaScript, this page runs marked for the parse and DOMPurify for the sanitising, which is the same pair worth using in a build of your own. Whichever you pick, the sanitising step is not optional if the Markdown came from anyone but you.

How to use

  1. 01Get the document into the left pane. Typing, pasting and Upload all work; Upload reads the file with the browser File API and nothing is sent anywhere.
  2. 02Leave Flavour on GitHub if the document is a README, an issue or anything GitHub renders. Switch to CommonMark when the destination is a stricter parser, and the panel will name any syntax that stops working.
  3. 03Switch between HTML and Preview above the output. They are the same string — the preview just renders it, inside a sandbox where nothing can execute.
  4. 04Copy takes the HTML, Download saves it. Set Output to Document first if you want a file that opens in a browser on its own.

A README into a project page

The docs live in Markdown and the site is plain HTML. Fragment output drops straight into the template, and GitHub flavour keeps the tables and badges working the way they do on the repo page.

Markdown
## Install

```sh
npm i thing
```

| Flag | Default |
| --- | --- |
| `--fast` | off |
HTML
<h2>Install</h2>
<pre><code class="language-sh">npm i thing
</code></pre>
<table>
<thead><tr><th>Flag</th><th>Default</th></tr></thead>
<tbody><tr><td><code>--fast</code></td><td>off</td></tr></tbody>
</table>

A document somebody sent you

An issue body, a form submission, a file from outside. Raw HTML is legal Markdown, so this is the case where the sanitiser earns its place — and the report names what came out rather than quietly dropping it.

Markdown
Thanks for the report.

<img src=x onerror="steal()">
HTML
<p>Thanks for the report.</p>
<img src="x">

<!-- report: 1 construct removed — onerror -->

Release notes for an email

Line breaks on, because the notes were written in a comment box where every newline showed as one, and the mail client will not re-flow them the way a rendered file would.

Markdown
Fixed:
login timeout
cache key collision
HTML
<p>Fixed:<br>login timeout<br>cache key collision</p>

A note to open in a browser

Document output wraps the fragment as a real file — doctype, charset, viewport and a title from the first heading — so Download gives you something that opens on its own rather than markup you still have to host.

Markdown
# Deploy runbook

1. Drain the node
HTML
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Deploy runbook</title>
</head>
<body>
<h1>Deploy runbook</h1>
<ol>
<li>Drain the node</li>
</ol>
</body>
</html>

Markdown syntax and the HTML it produces

You writeYou getWorth knowing
# Heading<h1>Heading</h1>The space after the hashes is required — #Heading is a paragraph
Heading then =====<h1>Heading</h1>The older setext form. Two dashes instead give an <h2>, which is why a line of dashes under text is not a horizontal rule
**bold** · *italic*<strong> · <em>Underscores work too, except inside a word — snake_case_name is safe
`code`<code>code</code>To show a literal backtick, fence with two: ``a ` b``
```js```<pre><code class="language-js">The language becomes a class, not styling. Highlighting is your stylesheet's job
Four leading spaces<pre><code>Only after a blank line at top level. The same four spaces under a list item continue the item instead
[text](url "title")<a href="url" title="title">The title is optional and goes in quotes inside the parentheses
[text][ref] plus [ref]: urlthe same <a>Reference style. The definition can be anywhere in the document and does not render
![alt](img.png)<img src="img.png" alt="alt">An exclamation mark and otherwise identical to a link
> quoted<blockquote>Nest by doubling the marker: >>
- a then two spaces then - ba nested <ul>Indentation makes the nesting. Mixing -, * and + at the same level starts a new list
| a | b | with | --- |<table>GitHub only. The delimiter row is what makes it a table; :--- and ---: set alignment
- [x] donea disabled <input type="checkbox">GitHub only, and always disabled — it is a rendering, not a form
~~struck~~<del>struck</del>GitHub only
Two spaces at end of line<br>The trailing spaces are invisible and editors strip them. A backslash at the end of the line does the same thing visibly
--- on its own line<hr>Needs a blank line above it, or it underlines the line before into a heading
\*literal\**literal*A backslash escapes any punctuation Markdown would otherwise read

Under the default settings: GitHub flavour, line breaks off, raw HTML sanitised, fragment output.

Where the two flavours disagree

The same inputGitHub gives youCommonMark gives you
a | b
--- | ---
1 | 2
a <table> with a header rowone <p> containing the pipes and dashes as text
~~struck~~<del>struck</del>~~struck~~, tildes and all
- [ ] todoa list item with a checkboxa list item reading [ ] todo
see https://example.comthe URL becomes an <a>the URL stays plain text — wrap it in <https://example.com> to link it
line one
line two
one paragraph, no break — same as CommonMark. Turn Line breaks on for <br>one paragraph, no break. This is where GitHub comment boxes differ from GitHub files
Everything elseidenticalidentical

These four constructs are the entire difference. A document using none of them converts identically under both, and the Flavour control changes nothing.

Tips

  • Check the flavour before you blame the document. If a table came out as a paragraph of pipes, the parser is on CommonMark and the fix is one control, not a rewrite.
  • Turn line breaks on only for text written in a comment box. GitHub renders a single newline as a break in comments and issues but not in .md files, and that inconsistency is where the expectation comes from.
  • Read the removal list on any document you did not write yourself. It names each construct the sanitiser took out, which is also a fair summary of what the sender was attempting.
  • Switch Raw HTML to Escaped when you want to see what a document contains rather than run it. Nothing is dropped in that mode — the tags come through as visible text.
  • Use Fragment for anything going into a template. Document mode adds a <head>, and a second <head> inside an existing page is invalid markup that browsers silently rearrange.
  • Send the result through the HTML formatter if you need it indented a particular way — this page emits the parser's own line breaks rather than imposing a house style on your output.

Gotchas

Raw HTML makes a Markdown file an executable document

Every dialect allows HTML in the source and every parser emits it verbatim — that is the specification, not a defect, and it is how <details> blocks and sized images get into a README. The consequence is that "convert this Markdown" and "run this HTML" are the same request, and a document from an issue tracker, a form or a colleague is untrusted input. Converters that render a preview without sanitising are one <img onerror> away from executing whatever the sender wanted. Ask what a converter does about this before pasting anything into it.

Three dialects share one file extension

Gruber's original had no grammar and left ambiguities the implementations resolved differently; CommonMark specified those cases; GitHub added tables, task lists, strikethrough and autolinks on top. Older parsers — the ones inside a wiki, a static site generator from 2014, a language's standard library — may implement any of the three or something between. A document that renders correctly on GitHub is not thereby portable, and the mismatch shows up as content silently rendering as literal text rather than as an error anyone will see.

A single newline is not a line break

Press Enter once in the middle of a paragraph and CommonMark joins the lines; so does GitHub, in a .md file. GitHub's comment boxes do the opposite, and since that is where most people meet Markdown, the expectation follows them to their files and is wrong there. The portable way to force a break is a backslash at the end of the line. Two trailing spaces also work and are worse, because they are invisible and most editors strip trailing whitespace on save.

Indentation decides between a code block and a list

Four spaces at the start of a line make a code block — after a blank line at the top level. The identical four spaces beneath a list item instead continue that item, and inside a list the threshold for a code block moves in by the list's own indent. This is why a snippet indented to line up under a bullet renders as prose, and why a paragraph you indented for readability turns into a grey box. Fenced blocks with backticks have none of this ambiguity, which is the reason to prefer them.

Server-side converters keep what you paste

A document going into a converter is often a draft, an internal runbook or a customer report. Searching for one of the best-known converters returns other people's saved conversions as ordinary results — pages titled "Documentation 1", "Manual", "Test" and "sample" sitting at public URLs under that tool's domain. Nobody chose to publish those. Find out where a converter executes before you trust it with anything. This one does the work in the browser you are reading it in, and a sweep over every function it uses runs with the network primitives replaced by throwing stubs, so a request would break the build rather than reach anybody.

Technical details

Parser
marked. It is a pinned npm dependency pulled in by dynamic import when you first convert something, which keeps it out of the initial download for anyone who only reads the page. Version, licence and update procedure are in THIRD_PARTY_NOTICES.md.
Flavour
GitHub Flavored Markdown by default — CommonMark plus tables, strikethrough, task lists and autolinks. CommonMark is selectable, and the panel names any construct it drops. Line breaks are a separate toggle and are inert under CommonMark, so the control is disabled rather than silently ignored.
Sanitisation
DOMPurify with the HTML profile, plus style attributes removed. Scripts, every on* handler, javascript: and data:text/html URLs, and the framing elements (iframe, object, embed, base, meta) do not survive. details, img, kbd, sup, table alignment and the language-* class marked puts on code blocks all do.
Console reports
A document containing a style attribute produces one style-src-attr report in the browser console. That is this site's Content Security Policy refusing to apply an inline style inside the scratch document the sanitiser parses into — the same attribute is then removed by the policy above, and the document is never displayed. It is not avoidable and not a defect: under style-src 'self' every route into a DOM reports it, DOMParser, <template> and setAttribute alike. Treat it as a third layer agreeing with the other two.
Preview
Rendered in a frame whose sandbox attribute is present and empty: no scripts, no same-origin access, no forms, no navigation. It is fed the same string Copy carries, so the two cannot disagree.
Output
Fragment by default. Document mode adds a doctype, lang, charset, viewport and a title taken from the first heading, and includes no stylesheet — output that arrives with somebody else's CSS is output you have to strip.
File handling
Uploads are read inside the page with the browser File API and are never transmitted; Download writes out what is already in the tab.
Limits
Input is refused above 2 MB, checked before the parser is even loaded, because the document exists as source, tokens, an HTML string and two separate DOM trees while this runs. The output pane stops drawing past 300,000 characters. Copy and Download are unaffected and always carry the complete result.
Network
None from tool code. A test sweep calls every function this page uses with fetch and XMLHttpRequest replaced by stubs that throw, so a stray request fails the build instead of shipping. Disconnect from the network and the page still works.

Frequently asked questions

Why is my table not rendering?

Because Flavour is set to CommonMark, which does not define tables — under it a table is a paragraph that happens to contain pipe characters. Switch to GitHub. This is the single most common problem with Markdown converters and it is almost never the document: tables, task lists, strikethrough and autolinks are GitHub additions, and a tool that implements CommonMark without saying so makes all four vanish without a word.

Is it safe to convert a document somebody sent me?

Here, yes, and the reason is worth understanding rather than trusting. Markdown permits raw HTML and every parser passes it through, so a hostile document is just an HTML document. Two things happen to it: the output is sanitised, so scripts, event handlers and javascript: URLs are gone from the string you copy; and the preview renders inside a sandboxed frame that cannot execute anything regardless. The work is JavaScript running in this tab. Every function it calls is covered by a test that stubs fetch and XMLHttpRequest to throw, so a request that slipped in would break the build rather than reach a server — and you can confirm it for yourself by disconnecting and carrying on.

What exactly gets removed?

Scripts, every on* event handler, javascript: and data:text/html URLs, and the framing elements — iframe, object, embed, base, meta. Also style attributes, which is a deliberate step beyond the sanitiser's defaults: they can carry a URL scheme, and inline style has no place in converted output anyway. Everything Markdown legitimately produces survives, including details, images with dimensions, table alignment and the language class on code blocks.

How do I convert Markdown to HTML in Python?

The markdown package: markdown.markdown(text) for the basics. It is CommonMark-ish by default and needs extensions=["tables", "fenced_code"] before it matches what GitHub does, which is the same dialect gap this page's Flavour control exposes. If the input is untrusted, follow it with bleach.clean() or an equivalent — markdown does not sanitise, and neither does any other parser.

What about Pandoc or the command line?

pandoc file.md -o file.html for a fragment, and pandoc -s file.md -o file.html for a standalone document — the -s flag is the same choice as the Output control here. Pandoc reads several Markdown dialects and you select one with -f gfm or -f commonmark, which is worth doing explicitly rather than accepting its default.

Can I get styled HTML rather than plain tags?

Not from here, on purpose. Converter output that arrives carrying a stylesheet is output you have to strip before it will fit anything you already have, and the CSS is invariably someone's idea of what a document looks like rather than yours. What you get is semantic markup with the code language preserved as a class — attach your own stylesheet and it will look like your site instead of like a converter.

Why is the line-break control disabled sometimes?

Because you have selected CommonMark, where the parser ties line-break handling to the GitHub extensions and the setting has no effect. Leaving the control clickable and silently ignoring it would be worse than showing it unavailable: a control that does nothing when you use it is a bug report waiting to happen.

Does the preview show exactly what I am copying?

Yes, and that is enforced rather than intended — the frame is fed the identical string. It is worth stating because a converter that sanitises the preview but not the output, or styles the preview so it looks better than the markup deserves, is misleading in the one place you are relying on it. What the preview does not carry is your site's CSS, so it renders in browser defaults; the structure is what you are checking.