React Markdown Kit vs markdown-to-jsx
markdown-to-jsx is a single-package Markdown toolchain with no runtime dependencies
that compiles to React, React Native, SolidJS, Vue, HTML, Markdown or an AST.
@react-markdown-kit/renderer is a React renderer built on micromark with the
react-markdown prop surface. They differ most on raw HTML and on how you extend them.
This page uses the same section order as every comparison page here: install size, GFM,
security defaults, streaming, server rendering, plugins, migration. Facts about
markdown-to-jsx come from its readme at version 9.10.3,
quantizor/markdown-to-jsx. Facts about the kit link to the
test or the generated table that produced them.
Install size
Bytes a browser downloads for one import, measured from published tarballs by
scripts/compare-bundles.mjs and
committed to
docs/data/bundle-sizes.json on
2026-09-20. React and React DOM are external in every row, minification is esbuild with
NODE_ENV set to production, and 1 KB is 1024 bytes.
| Import | Version | Minified | Gzipped | License | Peer react |
|---|---|---|---|---|---|
| markdown-to-jsx | 9.10.3 | 76.8 KB | 27.8 KB | MIT | >= 16.0.0 |
| @react-markdown-kit/renderer, CommonMark | 0.1.0 | 118.5 KB | 36.8 KB | MIT | >=18 |
| @react-markdown-kit/renderer + GFM preset | 0.1.0 | 158.1 KB | 48.5 KB | MIT | >=18 |
markdown-to-jsx wins this section by
9.0 KB gzipped against the kit on
CommonMark, and by 20.7 KB against the kit
with GFM enabled. Its npm metadata lists no runtime dependencies, so nothing else arrives
with it. The kit brings the micromark and
unified packages, which is what buys it the remark and rehype interface.
GFM
markdown-to-jsx parses GFM with no option and no plugin. Its readme states that all
GFM special syntaxes are supported, including tables, task lists, strikethrough,
autolinks and tag filtering.
The kit needs one line.
import Markdown, { defineMarkdownPreset, gfm } from '@react-markdown-kit/renderer'
const preset = defineMarkdownPreset({ extensions: [gfm()] })
<Markdown preset={preset}>{content}</Markdown>
// or the plugin route, which must produce the same markup
import remarkGfm from 'remark-gfm'
<Markdown remarkPlugins={[remarkGfm]}>{content}</Markdown>
What the kit can show is the conformance run.
tests/gfm.test.ts asserts 68 cases derived
from the GFM spec three ways: through gfm(), through remarkPlugins={[remarkGfm]}, and
through plain CommonMark, where the first two must agree after normalizing attribute
order, entity spelling and whitespace between block tags
(tests/helpers/html.ts) and the third
must not produce the GFM markup, unless the case is one of the 12 that assert something
is not GFM, which are named in the test so the exemption cannot spread.
tests/commonmark.test.ts runs every
example from CommonMark 0.31.2. This repository runs no conformance suite against
markdown-to-jsx, so its compliance is reported here as its readme states it and not as
a measurement.
| Package | Gzipped | | --- | ---: | | renderer | measured | - [x] task list - [ ] ~~strikethrough~~ and https://example.com
| Package | Gzipped |
|---|---|
| renderer | measured |
- task list
-
strikethroughand https://example.com
markdown-to-jsx for less setup. Kit for a published conformance run.
Security defaults
This is the section where the two packages disagree on purpose. Neither uses
dangerouslySetInnerHTML.
| Default | markdown-to-jsx 9.10.3 | React Markdown Kit |
|---|---|---|
| Raw HTML in the source | Parsed into JSX. disableParsingRawHTML defaults to false | Rendered as visible escaped text. Opt in with rehype-raw |
| Dangerous tags | tagfilter defaults to true, escaping script, iframe, style and similar | Not applicable by default, because no tag is mounted |
| Raw HTML sanitization | Documented as always on, independent of options.sanitizer | Not applicable by default |
| URLs | Built-in sanitizer, replaceable through options.sanitizer | Default urlTransform allows http, https, irc, ircs, mailto, xmpp and empties the rest |
| Custom components | overrides by tag name | components by tag name, trusted code in both |
Read the two positions in full:
raw HTML sanitization and
options.sanitizer on their side,
the security model on this one.
The practical question is what happens to HTML you did not write. On the kit it is text
until you add a plugin, which is asserted by
the renderer security tests:
raw HTML not executed, script tags escaped rather than emitted, javascript: URLs
blocked, obfuscated protocols blocked, dangerous image sources blocked, relative and
anchor URLs kept, and the same policy applied to a precompiled document so it is not a
bypass.
Inline <b>bold tag</b> and a <div>block</div>. <img src=x onerror="alert(1)">
Inline <b>bold tag</b> and a <div>block</div>.
<img src=x onerror="alert(1)">The tags are text. On markdown-to-jsx defaults, b and div would be real elements
and the img tag would mount as a real <img src="x"> with the onerror attribute
dropped. tagfilter does not cover img; it escapes script, iframe, style,
title, textarea, xmp, noembed, noframes and plaintext
(their readme).
Different goals. Pick markdown-to-jsx if authors are meant to write HTML in the
Markdown. Pick the kit if the Markdown can come from a user, a model or a CMS.
Streaming
Both packages address streaming, and they choose opposite behaviour for an incomplete construct.
markdown-to-jsx has an opt-in optimizeForStreaming option, false by default, which
its readme says suppresses incomplete structures until the closing delimiter arrives:
unclosed HTML tags, unclosed inline code, unclosed emphasis, unclosed strikethrough,
unclosed links and an incomplete table are held back, while a fenced code block shows its
content as it arrives
(streaming Markdown).
React Markdown Kit passes with no option to set, and the behaviour is tested.
packages/renderer/tests/streaming.test.tsx
is 59 tests that feed seven documents token by token and assert four properties on every
prefix: no prefix throws, the HTML of the closed prefix is byte-identical at every later
prefix, nothing in the closed prefix is duplicated, and the final prefix renders exactly
like the whole document rendered once. The same run is repeated through compileMarkdown
and through a mounted React root that grows token by token.
An incomplete construct renders as the characters typed so far. **str is the text
**str, [text]( is the text [text](, and a table is a paragraph until its delimiter
row is complete. Nothing is dropped and nothing is invented. Two prefixes legitimately
rewrite output that already rendered, and both are pinned by tests: a paragraph becomes a
heading when a setext underline arrives, and a GFM autolink points at the truncated host
while the URL is still being typed.
Which you want depends on the UI. Suppression hides the syntax and shows nothing; literal text shows the syntax and never moves the text above it. The kit's guarantee is the byte-stable closed prefix, which is what stops a long chat answer from reflowing.
Server rendering
Both render on the server. markdown-to-jsx documents that its Markdown component
detects a React Server Component environment and adapts with no 'use client' directive
(RSC usage), and it also
compiles to HTML, Markdown and an AST outside React entirely.
The kit is server-safe by construction. Its conformance suites call
renderToStaticMarkup, and
scripts/pack-check.mjs installs the
packed tarball into a consumer with no Lexical and renders there, so the renderer entry
pulls in no browser-only code
(server rendering).
The kit's extra is compileMarkdown. A MarkdownDocument compiled once renders many
times without parsing again: on the reference machine in
benchmarks/README.md a 10 KB document
takes 16.80 ms from a string and 5.99 ms precompiled, 2.8x faster.
Single-render speed against react-markdown@10.1.0, both sides parsing GFM, is the
honest counterweight: 1 KB is 1.21x slower (2.69 ms against 2.23 ms), 10 KB is at
parity, and 100 KB is 0.89x. The 1 KB row is an open regression, tracked in
benchmarks/README.md. No benchmark
against markdown-to-jsx is published here, so no speed claim is made against it.
Plugins
markdown-to-jsx has no remark or rehype interface. It extends through options:
overrides to swap the component for a tag name, renderRule for per-rule control,
sanitizer for URLs, slugify for heading ids, and createElement for another
framework. Its own AST is exported for anything else.
The kit takes the unified ecosystem directly: remarkPlugins, rehypePlugins and
remarkRehypeOptions, compared prop for prop against react-markdown at 4 of 4, 3 of 3
and 3 of 3 identical
(docs/COMPATIBILITY.md). On top of
that it has extensions, which register micromark syntax, mdast handlers, hast handlers
and editor behaviour in one declaration, so the renderer, the editor and a server that
only compiles the document all read the same thing
(extensions). @react-markdown-kit/mermaid,
@react-markdown-kit/slides and @react-markdown-kit/template are built that way.
Kit, if you want existing remark or rehype plugins or an editor.
markdown-to-jsx, if overrides are all you need and you want to stay at zero
dependencies.
Migration
There is no codemod for this direction. rmk-migrate and rmk-compare handle
react-markdown only
(compare.mjs), so
moving from markdown-to-jsx is a hand edit. The shape of it:
-import Markdown from 'markdown-to-jsx'
+import Markdown, { defineMarkdownPreset, gfm } from '@react-markdown-kit/renderer'
+
+const preset = defineMarkdownPreset({ extensions: [gfm()] })
-<Markdown options={{ overrides: { a: Anchor } }}>{content}</Markdown>
+<Markdown preset={preset} components={{ a: Anchor }}>{content}</Markdown>
Three things to check before you start.
- Raw HTML stops rendering. Anything relying on HTML inside the Markdown becomes
text unless you add
rehype-rawand a sanitizer, in that order. optionssplits into props.overridesbecomescomponents,sanitizerbecomesurlTransform, andwrapperandforceInlinehave no equivalent.renderRulehas no equivalent. Per-rule rendering is a rehype plugin or an extension here.
Diff your own corpus through both before committing. Render the same files with each
package and compare the HTML, the way
tests/compatibility.test.tsx
does for react-markdown.
FAQ
- Is markdown-to-jsx smaller than React Markdown Kit?
- Yes. One import of markdown-to-jsx 9.10.3 is smaller gzipped than one import of @react-markdown-kit/renderer 0.1.0, and markdown-to-jsx declares no runtime dependencies. The measurement is in docs/data/bundle-sizes.json, written by scripts/compare-bundles.mjs.
- Does markdown-to-jsx render raw HTML by default?
- Yes. Its readme documents disableParsingRawHTML defaulting to false, so HTML in the Markdown is parsed into JSX, with tagfilter on by default escaping script, iframe, style and similar tags. React Markdown Kit does the opposite by default: raw HTML stays visible escaped text until you add rehype-raw yourself.
- Which one should I use for a React Markdown renderer?
- Use markdown-to-jsx for the smallest bundle, zero dependencies, or output targets other than React. Use React Markdown Kit when you want the remark and rehype plugin surface, raw HTML inert by default, streaming tested prefix by prefix, or an editor and diagram plugins reading the same compiled document.
Next
Renderer playground · @react-markdown-kit/renderer on npm · Source on GitHub
react-markdown alternative · vs react-markdown · vs Streamdown · Renderer overview · Security model