Skip to main content

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.

One import, minified and gzipped
ImportVersionMinifiedGzippedLicensePeer react
react-markdown, CommonMark10.1.0115.0 KB35.5 KBMIT>=18
@react-markdown-kit/renderer, CommonMark0.1.0118.5 KB36.8 KBMIT>=18
react-markdown + remark-gfm10.1.0152.6 KB46.4 KBMIT>=18
@react-markdown-kit/renderer + GFM preset0.1.0158.1 KB48.5 KBMIT>=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.

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

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.

Defaultreact-markdown 10.1.0React Markdown Kit
dangerouslySetInnerHTMLNot used. The readme leads with it.Not used.
Raw HTML in the sourceRendered as visible text; rehype-raw to opt inSame, and skipHtml defaults to false in both
javascript: in a linkEmptied by the default urlTransformSame algorithm, same result on every probe URL
Plugins and componentsTrusted code, not sandboxedTrusted 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