Skip to main content

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.

One import, minified and gzipped
ImportVersionMinifiedGzippedLicensePeer react
markdown-to-jsx9.10.376.8 KB27.8 KBMIT>= 16.0.0
@react-markdown-kit/renderer, CommonMark0.1.0118.5 KB36.8 KBMIT>=18
@react-markdown-kit/renderer + GFM preset0.1.0158.1 KB48.5 KBMIT>=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.

GFM on this page, rendered by @react-markdown-kit/renderer
Markdown
| Package | Gzipped |
| --- | ---: |
| renderer | measured |

- [x] task list
- [ ] ~~strikethrough~~ and https://example.com
Rendered
PackageGzipped
renderermeasured

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.

Defaultmarkdown-to-jsx 9.10.3React Markdown Kit
Raw HTML in the sourceParsed into JSX. disableParsingRawHTML defaults to falseRendered as visible escaped text. Opt in with rehype-raw
Dangerous tagstagfilter defaults to true, escaping script, iframe, style and similarNot applicable by default, because no tag is mounted
Raw HTML sanitizationDocumented as always on, independent of options.sanitizerNot applicable by default
URLsBuilt-in sanitizer, replaceable through options.sanitizerDefault urlTransform allows http, https, irc, ircs, mailto, xmpp and empties the rest
Custom componentsoverrides by tag namecomponents 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.

Raw HTML through @react-markdown-kit/renderer, default settings
Markdown
Inline <b>bold tag</b> and a <div>block</div>.

<img src=x onerror="alert(1)">
Rendered

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-raw and a sanitizer, in that order.
  • options splits into props. overrides becomes components, sanitizer becomes urlTransform, and wrapper and forceInline have no equivalent.
  • renderRule has 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