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 props. The biggest differences are how they handle raw HTML and how you
extend them.
This page follows the same section order as the other comparison pages (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 is smaller, 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 pulls in the micromark and unified packages, which is where the remark and
rehype interface comes from.
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 extra 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>
On the kit side there's a conformance run you can look at.
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 (those are named in the test so the exemption can't 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 the way its readme states it, and we
haven't measured it.
| 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 takes less setup. The kit has a published conformance run.
Security defaults
This is where the two packages deliberately differ. 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 |
You can read both positions in full in
raw HTML sanitization and
options.sanitizer on their side,
the security model on this one.
In practice it comes down to what happens to HTML you didn't write. On the kit it stays text
until you add a plugin, which is covered by
the renderer security tests:
raw HTML isn't executed, script tags are escaped instead of emitted, javascript: URLs are
blocked, obfuscated protocols blocked, dangerous image sources blocked, relative and
anchor URLs are kept, and the same policy applies to a precompiled document so that can't
be used to get around it.
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 come out as 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).
They're built for different situations. If authors are meant to write HTML in the Markdown,
markdown-to-jsx is the better fit. If the Markdown can come from a user, a model or a CMS,
I'd go with the kit.
Streaming
Both packages handle streaming, but they do opposite things with 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 needs no option for this, 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 gets dropped or made up. Two prefixes do 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 one you want depends on the UI. Suppression hides the syntax until it's complete, while literal text shows the syntax and never moves the text above it. What the kit guarantees is that the closed prefix stays byte-stable, which keeps 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 built to run on the server too. 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 also has 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.
On the other hand, single-render speed against react-markdown@10.1.0 (both sides parsing
GFM) is 1.21x slower at 1 KB (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 this page doesn't make a speed claim 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 works with the unified ecosystem directly, through 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.
The kit is the better fit if you want existing remark or rehype plugins or an editor.
markdown-to-jsx is fine 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. It looks roughly like this.
-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>
A few 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.
I'd recommend diffing 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 goes the other way by default and shows raw HTML as escaped text until you add rehype-raw yourself.
- Which one should I use for a React Markdown renderer?
- I'd pick markdown-to-jsx if you want the smallest bundle, zero dependencies, or output targets other than React. React Markdown Kit makes more sense 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