React Markdown Kit vs react-markdown
Both turn a Markdown string into React elements, both parse with micromark, and neither
uses dangerouslySetInnerHTML. react-markdown is the one most projects already have.
This page goes through them section by section, in the same order as the other comparison
pages (install size, GFM, security defaults, streaming, server rendering, plugins,
migration).
Facts about react-markdown come from its repository,
remarkjs/react-markdown, at version 10.1.0. Facts about the
kit link to the test or the generated table that produced them.
If react-markdown works for you and you care about the last kilobyte, I'd stay on it.
The kit is probably worth a look if you stream Markdown from a model, render the same
document more than once, or want the editor and plugins to read the same document the
renderer reads. The alternative page covers
the three differences in full.
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 |
|---|---|---|---|---|---|
| react-markdown, CommonMark | 10.1.0 | 115.0 KB | 35.5 KB | MIT | >=18 |
| @react-markdown-kit/renderer, CommonMark | 0.1.0 | 118.5 KB | 36.8 KB | MIT | >=18 |
| react-markdown + remark-gfm | 10.1.0 | 152.6 KB | 46.4 KB | MIT | >=18 |
| @react-markdown-kit/renderer + GFM preset | 0.1.0 | 158.1 KB | 48.5 KB | MIT | >=18 |
react-markdown is smaller here. It is
1.3 KB smaller gzipped on CommonMark and
2.1 KB smaller with GFM on both
sides, so size isn't a reason to switch.
GFM
react-markdown parses GitHub Flavored Markdown when you pass the remark-gfm plugin,
which its readme documents. The kit supports that same plugin and also has a native extension.
// react-markdown
import Markdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
<Markdown remarkPlugins={[remarkGfm]}>{content}</Markdown>
// React Markdown Kit
import Markdown, { defineMarkdownPreset, gfm } from '@react-markdown-kit/renderer'
const preset = defineMarkdownPreset({ extensions: [gfm()] })
<Markdown preset={preset}>{content}</Markdown>
Tests check that both routes agree.
tests/gfm.test.ts runs 68 cases derived
from the GFM spec three ways: through the native gfm() extension, through
remarkPlugins={[remarkGfm]}, and through plain CommonMark. 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, so a case that would pass without GFM cannot sit in the
corpus unless it is one of the 12 cases that assert something is not GFM, which are
named in the test so the exemption cannot spread.
tests/compatibility.test.tsx
compares remark-gfm output between the two packages directly, and the four
remarkPlugins cases match.
| Package | Gzipped | | --- | ---: | | renderer | measured | - [x] task list - [ ] ~~strikethrough~~ and https://example.com
| Package | Gzipped |
|---|---|
| renderer | measured |
- task list
-
strikethroughand https://example.com
About even. The extension saves you a dependency, and the plugin route still works if you'd rather use it.
Security defaults
The defaults are the same, and the tests below are what that's based on.
| Default | react-markdown 10.1.0 | React Markdown Kit |
|---|---|---|
dangerouslySetInnerHTML | Not used. The readme leads with it. | Not used. |
| Raw HTML in the source | Rendered as visible text; rehype-raw to opt in | Same, and skipHtml defaults to false in both |
javascript: in a link | Emptied by the default urlTransform | Same algorithm, same result on every probe URL |
| Plugins and components | Trusted code, not sandboxed | Trusted code, not sandboxed |
Evidence on this side:
the renderer security tests
cover raw HTML, script tags, javascript: URLs, obfuscated protocols, image sources,
relative URLs, allowedElements, and the policy applying to a precompiled document so it
can't be bypassed. The urlTransform and skipHtml rows of
docs/COMPATIBILITY.md are the
cross-package proof: 4 of 4 and 4 of 4 identical. The policy is described in
the security model.
Both readmes say the same thing about plugins: a remark plugin, a rehype plugin or a
component you pass in is your code, and you should add rehype-sanitize when the content
isn't yours.
About even.
Streaming
A model writes Markdown a token at a time, so a chat UI hands the renderer a new prefix on every token and most prefixes are not valid Markdown.
React Markdown Kit has tests for this.
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.
The cases are an unclosed code fence, half-written emphasis and strong, a table mid-row,
a list mid-item, a heading with no trailing newline, a link with an unclosed bracket, and
a fenced block that closes late. Half-written constructs render as the characters typed
so far, so **str shows up as the text **str instead of a broken element. 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. Both are correct CommonMark and GFM
behaviour.
This repository runs no streaming tests against react-markdown, and its readme
documents no streaming-specific behaviour, so this page doesn't claim anything about how
it handles partial input. If that matters to you, run the same corpus against it yourself.
Based on what this repository tests, the kit has the edge here.
Server rendering
Both render on the server with no DOM. The kit's own suites call
renderToStaticMarkup throughout, including
the CommonMark conformance suite, 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.
The difference is asynchronous work. react-markdown 10 exports MarkdownAsync and
MarkdownHooks so a plugin can await something. The kit has neither and is synchronous
only, which is listed as a difference in
docs/COMPATIBILITY.md.
The kit adds a second path: compile once, render many times.
compileMarkdown returns a MarkdownDocument that <Markdown document={...} />
renders 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 from a precompiled document, 2.8x faster,
because parsing is about two thirds of the work.
For a single render it goes the other way. Both sides parsing GFM, same process, median
after five warm-up calls: 1 KB is 1.21x slower than react-markdown (2.69 ms against
2.23 ms), 10 KB is at parity (16.22 ms against 16.25 ms), and 100 KB is 0.89x (206.96 ms
against 233.36 ms). The 1 KB row is an open regression: it is fixed per-render cost, and
the fix, caching the processor per resolved configuration, is tracked in
benchmarks/README.md and not done.
Reproduce with pnpm bench.
Use react-markdown if you need async plugins, and the kit if you render the same document repeatedly.
Plugins
react-markdown takes remarkPlugins, rehypePlugins and remarkRehypeOptions. So
does the kit, and the compatibility run compares them directly: 4 of 4, 3 of 3 and 3 of 3
identical.
The kit adds extensions. An extension registers micromark syntax, mdast handlers, hast
handlers and editor behaviour together, so the same declaration serves the renderer, the
editor and a server that only compiles the document
(extensions). @react-markdown-kit/mermaid,
@react-markdown-kit/slides and @react-markdown-kit/template are built this way.
react-markdown doesn't have an equivalent, since it has no editor to share with.
One thing that applies to both and is easy to miss is that a plugin that changes the dialect cannot apply to a document that was already compiled without it. Compile with the extensions you render with, or pass the string (compiling a document).
The kit is ahead if you need an editor or a diagram plugin. Otherwise it's about even.
Migration
There are two commands for this. Both run locally and ship in the renderer package.
npx rmk-compare 'content/**/*.md' --gfm # diff your own corpus through both
npx rmk-migrate 'src/**/*.tsx' # report
npx rmk-migrate 'src/**/*.tsx' --write # apply
rmk-migrate edits import statements and nothing else, preserves your alias and quote
style, and won't touch MarkdownAsync, MarkdownHooks and
react-markdown/lib/*. It flags rehype-raw without a sanitizer instead of carrying
that setup across. Every refusal is a test in
tests/codemod.test.ts, and
rmk-compare exits non-zero on a difference
(tests/compare-runner.test.ts).
There are three things the codemod can't fix. They're listed on the alternative page and covered step by step in the migration guide, which is why we don't call this a drop-in replacement.
FAQ
- Which is smaller, react-markdown or React Markdown Kit?
- react-markdown is. One import of react-markdown 10.1.0 is smaller gzipped than one import of @react-markdown-kit/renderer 0.1.0, and the gap is about the same with GFM on both sides. Both parse with micromark, so the extra size is the kit's document contract and policy layer. The numbers are in docs/data/bundle-sizes.json.
- Do both render raw HTML safely by default?
- Yes. Neither uses dangerouslySetInnerHTML, both render raw HTML as visible text unless you add rehype-raw, and both empty javascript: URLs with the same default urlTransform algorithm. 39 of 39 compared cases across 11 props produce identical markup.
- Can I switch from react-markdown without rewriting my props?
- For the most part. The prop names and meanings match, and npx rmk-migrate only rewrites the import statements. Three things still differ: className is ignored instead of throwing, MarkdownAsync and MarkdownHooks don't exist, and a dialect plugin can't apply to a document that was compiled without it. So I wouldn't treat it as a drop-in replacement.
Next
Renderer playground · @react-markdown-kit/renderer on npm · Source on GitHub
react-markdown alternative · vs markdown-to-jsx · vs Streamdown · Migration guide · Compatibility matrix · Security model