react-markdown alternative, with a codemod
@react-markdown-kit/renderer renders Markdown as React elements with the same prop
names and the same meanings as react-markdown 10. It is not a drop-in replacement.
Three things differ, and two of them can bite you silently, so this page lists the
evidence before it lists the reasons.
npm install @react-markdown-kit/renderer
import Markdown from '@react-markdown-kit/renderer'
<Markdown>{content}</Markdown>
Release notes
Ship safe output with no plugin list.
| Package | Gzipped |
|---|---|
| renderer | measured |
| editor | measured |
- raw HTML rendered as text
-
javascript:links emptied
Try it: <img src=x onerror="alert(1)"> and click.
The img tag and the script-shaped text stay text. The javascript: destination is
emptied. Neither needed a plugin.
What matches
docs/COMPATIBILITY.md is generated by the test run, so it cannot drift from the code.
Identical input goes through both packages and the normalized HTML is compared. Against
react-markdown@10.1.0: 39 comparisons across 11 props, 39 produce identical markup
(tests/compatibility.test.tsx,
generated matrix).
| Prop | Cases matching |
|---|---|
children | 4 of 4 |
components | 4 of 4 |
remarkPlugins | 4 of 4 |
rehypePlugins | 3 of 3 |
remarkRehypeOptions | 3 of 3 |
allowedElements | 4 of 4 |
disallowedElements | 3 of 3 |
allowElement | 3 of 3 |
unwrapDisallowed | 3 of 3 |
urlTransform | 4 of 4 |
skipHtml | 4 of 4 |
The default URL policy is the same algorithm and agrees on every probe URL, including
javascript: and mailto:. skipHtml has the same default, false, in both packages.
The three differences
className is ignored, not rejected
react-markdown 10 throws Unexpected className prop, remove it. This renderer has no
className prop either, but ignores it at runtime. TypeScript rejects it, so TypeScript
code fails at build time. JavaScript code that still passes className loses the wrapper
with no error. Wrap the component instead.
-<Markdown className="prose">{content}</Markdown>
+<div className="prose">
+ <Markdown>{content}</Markdown>
+</div>
MarkdownAsync and MarkdownHooks do not exist
This renderer is synchronous. react-markdown 10 exports MarkdownAsync and
MarkdownHooks for plugins that do asynchronous work. There is no equivalent export
here, and an async remark or rehype plugin will not run. The codemod leaves those imports
alone and says why (codemod tests).
A dialect plugin cannot apply to a precompiled document
react-markdown only ever takes a string, so every plugin runs at parse time. This
renderer also accepts a MarkdownDocument that was compiled earlier. A plugin that
changes the dialect, such as remark-gfm, cannot add tables to text that has already
been parsed. Tree-transforming plugins still run. Compile with the same extensions you
render with, or pass the string
(compiling a document).
The codemod
The renderer package ships two commands. Both run on your machine and send nothing anywhere.
npx rmk-compare 'content/**/*.md' --gfm # render your corpus through both, diff it
npx rmk-migrate 'src/**/*.tsx' # report what would change
npx rmk-migrate 'src/**/*.tsx' --write # apply it
rmk-migrate is a dry run by default and edits import statements only. It keeps your
alias and your quote style.
-import ReactMarkdown from "react-markdown"
+import ReactMarkdown from "@react-markdown-kit/renderer"
It refuses to guess. MarkdownAsync, MarkdownHooks and imports that reach into
react-markdown/lib/* are left untouched with an explanation, and rehype-raw used
without a sanitizer is flagged rather than carried across, because that pair executes
author HTML. Each of those refusals is a test in
tests/codemod.test.ts.
rmk-compare exits non-zero on a difference, so a pull request can gate on it
(tests/compare-runner.test.ts).
3 document(s) compared
matching 3 (100%)
differing 0
Benchmarks, including the regression
Server rendering with renderToStaticMarkup, both sides parsing GFM, median of the
printed iteration count after five warm-up calls. Reference machine: Apple M4 Max on
Node 24.17. Baseline is react-markdown@10.1.0. Ratios are kit divided by baseline, so
below 1.00 is faster. Reproduce with pnpm bench
(benchmarks/run.mjs,
method and results).
| Document | Kit median | Baseline median | Ratio |
|---|---|---|---|
| 1 KB | 2.69 ms | 2.23 ms | 1.21x slower |
| 10 KB | 16.22 ms | 16.25 ms | 1.00x |
| 100 KB | 206.96 ms | 233.36 ms | 0.89x |
The 1 KB row is a regression against react-markdown and it is open. It is fixed
per-render cost, not a scaling problem: building the unified processor and walking the
content policy happens once per render whatever the document size. The fix is to cache
the processor per resolved configuration. It is tracked in
benchmarks/README.md and not done.
Re-rendering a document that was compiled once is the path that is faster on any size.
| Path | 10 KB median |
|---|---|
| From a source string | 16.80 ms |
From a precompiled MarkdownDocument | 5.99 ms |
Parsing is about two thirds of the work, and compileMarkdown does it once.
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. 1 KB is 1024 bytes.
| Import | Version | Minified | Gzipped |
|---|---|---|---|
| react-markdown, CommonMark | 10.1.0 | 115.0 KB | 35.5 KB |
| @react-markdown-kit/renderer, CommonMark | 0.1.0 | 118.5 KB | 36.8 KB |
| react-markdown + remark-gfm | 10.1.0 | 152.6 KB | 46.4 KB |
| @react-markdown-kit/renderer + GFM preset | 0.1.0 | 158.1 KB | 48.5 KB |
The renderer is larger than react-markdown, not smaller:
1.3 KB gzipped more on CommonMark and
2.1 KB more with GFM. Both parse
with micromark, so the difference is the document contract and the policy layer, not the
parser. Size is not a reason to switch.
When to stay on react-markdown
Stay if any of these apply. They are the honest cases, not a disclaimer.
- You use
MarkdownAsyncorMarkdownHooks, or an async remark or rehype plugin. There is no equivalent here. - Your smallest documents are on a hot path. The 1 KB case is 1.21x slower and the fix is not shipped.
- Every byte counts and you render CommonMark only.
react-markdownis 1.3 KB smaller gzipped. - You pass
classNamefrom JavaScript and cannot audit the call sites. TypeScript catches it; JavaScript does not. - The setup works and nothing on this page is a problem you have. A renderer swap is risk with no reward if the current one is fine.
When to switch
- You want raw HTML rendered as text and unsafe URLs emptied with no plugin list (security tests, security model).
- You stream Markdown from a model and need every prefix of the document to render
without throwing and without rewriting what is already on screen. That is 59 tests in
streaming.test.tsx. - You render the same document more than once and want to parse it once (precompiled documents).
- You want the editor, the Mermaid plugin, the slides plugin and the template plugin to read the same compiled document as the renderer (extensions).
FAQ
- Is React Markdown Kit a drop-in replacement for react-markdown?
- No, and docs/COMPATIBILITY.md says so in its first paragraph. The prop surface matches on 39 of 39 compared cases across 11 props, but className is ignored instead of throwing, MarkdownAsync and MarkdownHooks do not exist, and a dialect plugin such as remark-gfm cannot apply to a document that was already compiled without it.
- How do I switch from react-markdown to React Markdown Kit?
- Run npx rmk-compare on your own Markdown to see whether the output differs, then npx rmk-migrate as a dry run, fix the className call sites it reports, and apply with --write. The codemod changes import statements and nothing else, and it refuses to touch MarkdownAsync, MarkdownHooks and react-markdown internals.
- Is React Markdown Kit smaller than react-markdown?
- No. One import of the renderer is larger than one import of react-markdown, measured by scripts/compare-bundles.mjs and recorded in docs/data/bundle-sizes.json. Both parse with micromark, so size is not a reason to switch.
- Is React Markdown Kit faster than react-markdown?
- It depends on the document. On the reference machine in benchmarks/README.md it is 1.21x slower on a 1 KB document, at parity on 10 KB and 0.89x on 100 KB. A precompiled document re-renders 2.8x faster than parsing the same string again.
Next
Renderer playground · @react-markdown-kit/renderer on npm · Source on GitHub
Side by side with react-markdown · Side by side with markdown-to-jsx · Side by side with Streamdown · Migration guide · Renderer overview · Compatibility matrix