Guide
marked vs remark vs markdown-it in 2026: Choose a Markdown Parser
Compare marked, remark, and markdown-it by output, extension model, MDX needs, and safe rendering of untrusted Markdown.
Contents
The short answer
Choose marked when your job is to turn Markdown into HTML and its renderer hooks cover the changes you need. Choose markdown-it when you need direct control over parsing rules, tokens, or rendering, particularly for an editor preview or a custom Markdown dialect. Choose remark when your pipeline needs a Markdown syntax tree that several plugins can inspect or transform. If the source contains MDX components or expressions, use an MDX-aware pipeline built on the unified ecosystem; plain Markdown parsing is insufficient.
None of those choices makes untrusted rendered HTML safe by itself. Treat security as a separate output policy, then test that policy with hostile input.
| Requirement | Good starting point | Reason |
|---|---|---|
| Render ordinary Markdown to HTML | marked | Small API with renderer and token hooks |
| Customize syntax or token rendering | markdown-it | Rules and renderer operate on its token stream |
| Analyze or rewrite Markdown structure | remark | Plugins work on the Markdown AST (mdast) |
| Compile MDX | MDX compiler with remark plugins | MDX has JSX and JavaScript syntax beyond Markdown |
| Render untrusted user input | Any of the above, plus a tested sanitizer | Parsing and sanitizing solve different problems |
These are workflow choices, not a speed ranking. Parser throughput depends on the document, enabled extensions, runtime, and whether you include the complete HTML pipeline. Benchmark your own workload before selecting a library for latency alone. For a wider survey of libraries, see best Markdown parsing libraries; this page compares the three parser architectures directly.
marked: direct HTML rendering
The marked documentation describes marked.parse() and extensions for token walking, tokenization, and rendering. For a simple conversion:
import { marked } from 'marked'
const html = marked.parse('# Release notes\n\n**Ready** for review.')
console.log(html)
marked.parse() can return a string or a promise when asynchronous extensions are configured. A synchronous call is straightforward when no async extension is installed. Prefer a separate Marked instance when different pages need different rules: marked.use() modifies shared defaults and can let one pipeline's extensions affect another.
import { Marked } from 'marked'
const preview = new Marked({ gfm: true, breaks: false })
const html = preview.parse('| Name | Status |\n| --- | --- |\n| pkg | ready |')
The token hooks are useful for changing a few output elements. Once you need several passes that inspect parent and sibling nodes, a Markdown AST is easier to reason about than a collection of renderer overrides. Marked's own security guidance says it does not sanitize its HTML output.
markdown-it: configurable tokens and rules
markdown-it parses Markdown into tokens and renders HTML. Its MarkdownIt instance can use plugins and override rendering rules. The core API supports options such as html, linkify, and typographer; decide each option for your input and output context.
import MarkdownIt from 'markdown-it'
const md = new MarkdownIt({
html: false,
linkify: true,
typographer: false,
})
const html = md.render('# Release notes\n\nA link to https://example.com')
console.log(html)
html: false keeps raw HTML in the Markdown source from becoming live HTML. That is a helpful default, but it does not remove the need to review plugins, URL handling, and the final insertion into the page. If you enable html: true, sanitize the rendered result before showing untrusted content.
Use markdown-it when a rule or token-level extension is a good fit. Plugins can add syntax such as footnotes or containers, but each plugin changes both the accepted input and the output you must review. Its token stream is useful for rendering decisions; it is not the same interface as remark's mdast tree.
remark: a Markdown syntax tree
remark runs on unified. remark-parse produces mdast, so plugins can inspect and change headings, links, tables, and other Markdown nodes before HTML conversion. remark-rehype then changes the tree to HTML's hast format, and rehype-stringify serializes it.
import { unified } from 'unified'
import remarkParse from 'remark-parse'
import remarkRehype from 'remark-rehype'
import rehypeStringify from 'rehype-stringify'
const file = await unified()
.use(remarkParse)
.use(remarkRehype)
.use(rehypeStringify)
.process('# Release notes\n\n**Ready** for review.')
console.log(String(file))
Add remark-gfm if your documents need GitHub-flavored features such as tables and task lists. Remark plugins run before remark-rehype, on the Markdown tree. Rehype plugins run after it, on the HTML tree. That distinction matters when choosing a heading transformer, syntax highlighter, or sanitizer.
For MDX, use the MDX compiler and its remark or rehype plugin options. MDX adds JSX, expressions, and imports; compiling it requires a trust decision about executable code. A regular remark-to-HTML pipeline does not become an MDX compiler merely by adding a Markdown plugin.
Untrusted Markdown and HTML
The three libraries have different raw-HTML defaults, so "none sanitize" is too broad a description. Marked explicitly does not sanitize its output. Markdown-it disables raw HTML by default. In a basic remark-to-rehype pipeline, raw HTML from Markdown is not passed through as live markup unless dangerous-HTML handling is enabled. Plugins and custom renderers can change these properties.
If your product accepts untrusted Markdown, define which HTML elements and URL schemes may reach the browser, then test the whole pipeline. The remark-rehype security example shows rehype-raw followed by rehype-sanitize when raw HTML must be supported:
import { unified } from 'unified'
import remarkParse from 'remark-parse'
import remarkRehype from 'remark-rehype'
import rehypeRaw from 'rehype-raw'
import rehypeSanitize from 'rehype-sanitize'
import rehypeStringify from 'rehype-stringify'
const file = await unified()
.use(remarkParse)
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeRaw)
.use(rehypeSanitize)
.use(rehypeStringify)
.process(untrustedMarkdown)
const safeHtml = String(file)
Only enable that raw-HTML path when your product needs it. A simpler pipeline with raw HTML disabled reduces the surface to review. For marked or markdown-it, a separate HTML sanitizer can be applied after rendering; configure it for the HTML your product allows and test links, images, attributes, and extension output. Sanitizing the Markdown input alone does not establish that the rendered HTML is safe.
How to choose in a project
Start with the output your application needs. For a static, trusted document that only needs HTML, try the small marked example. For an interactive editor that changes block syntax or presentation, try markdown-it's rules and token rendering. For linting, link rewriting, content extraction, or a multi-stage publishing pipeline, build a small remark processor and keep its plugin order explicit. For MDX, start with the compiler and decide who is allowed to author executable content.
Run each candidate on representative documents, including malformed syntax and user input if relevant. Compare the final output, extension maintenance, bundle cost, and measured end-to-end latency. Package download totals and universal "fastest" labels cannot answer those questions.
Sources checked September 25, 2026: Marked advanced usage, markdown-it repository, remark repository, remark-rehype security guidance, and MDX documentation.
Compare the packages on PkgPulse.