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 isn't a drop-in replacement,
though. Three things differ, and two of them can fail silently, so this page goes through
the evidence first and the reasons to switch after.
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 as text, and the javascript:
destination gets emptied. Neither of those needed a plugin.
What matches
docs/COMPATIBILITY.md is generated by the test run, so it stays in sync with the code.
The same 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 uses the same algorithm and agrees on every probe URL, including
javascript: and mailto:. skipHtml defaults to false in both packages.
The three differences
className is ignored instead of throwing
react-markdown 10 throws Unexpected className prop, remove it. This renderer doesn't
have a className prop either, but it ignores it at runtime. The types reject it, so
TypeScript code fails at build time. JavaScript code that still passes className just
loses the wrapper without an error. Wrap the component instead.
-<Markdown className="prose">{content}</Markdown>
+<div className="prose">
+ <Markdown>{content}</Markdown>
+</div>
MarkdownAsync and MarkdownHooks don't exist
This renderer is synchronous. react-markdown 10 exports MarkdownAsync and
MarkdownHooks for plugins that do asynchronous work. There's no equivalent export
here, and async remark or rehype plugins won't run. The codemod leaves those imports
alone and tells you why (codemod tests).
A dialect plugin can't 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, like remark-gfm, can't add tables to text that's 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 locally and don't send anything 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 does a dry run by default and only edits import statements. It keeps
your alias and your quote style.
-import ReactMarkdown from "react-markdown"
+import ReactMarkdown from "@react-markdown-kit/renderer"
It won't guess. MarkdownAsync, MarkdownHooks and imports that reach into
react-markdown/lib/* are left alone with an explanation. rehype-raw used without a
sanitizer gets flagged instead of carried across, because that combination executes
author HTML. Each of those cases has a test in
tests/codemod.test.ts.
rmk-compare exits non-zero when it finds a difference, so you can gate a pull request
on it
(tests/compare-runner.test.ts).
3 document(s) compared
matching 3 (100%)
differing 0
Benchmarks, including the regression
These are server renders with renderToStaticMarkup, both sides parsing GFM, taking the
median of the printed iteration count after five warm-up calls. The reference machine is
an Apple M4 Max on Node 24.17, and the baseline is react-markdown@10.1.0. Ratios are
kit divided by baseline, so anything below 1.00 is faster. You can reproduce them 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's still open. It
comes from a fixed per-render cost and doesn't get worse with size. Building the unified
processor and walking the content policy happens once per render no matter how big the
document is. The fix is to cache the processor per resolved configuration. It's tracked
in benchmarks/README.md and hasn't
been done yet.
Re-rendering a document that was compiled once is faster at every size.
| Path | 10 KB median |
|---|---|
| From a source string | 16.80 ms |
From a precompiled MarkdownDocument | 5.99 ms |
Parsing is roughly two thirds of the work, and compileMarkdown only does it once.
Install size
This is how many 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. It's
1.3 KB gzipped more on CommonMark and
2.1 KB more with GFM. Both parse
with micromark, so the extra size comes from the document contract and the policy layer.
Size isn't a reason to switch.
When to stay on react-markdown
I'd stay if any of these apply to you.
- You use
MarkdownAsyncorMarkdownHooks, or an async remark or rehype plugin. There's no equivalent here. - Your smallest documents are on a hot path. The 1 KB case is 1.21x slower and the fix hasn't shipped.
- Every byte counts and you render CommonMark only.
react-markdownis 1.3 KB smaller gzipped. - You pass
classNamefrom JavaScript and can't audit the call sites. TypeScript catches it, JavaScript doesn't. - Your current setup works and nothing on this page is a problem you have. Swapping renderers is some risk for no real gain if the one you have 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's already on screen. There are 59 tests
for that 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 props match on 39 of 39 compared cases across 11 props, but className is ignored instead of throwing, MarkdownAsync and MarkdownHooks don't exist, and a dialect plugin such as remark-gfm can't 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 run npx rmk-migrate as a dry run, fix the className call sites it reports, and apply with --write. The codemod only changes import statements, and it won't touch MarkdownAsync, MarkdownHooks or 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 isn't a reason to switch.
- Is React Markdown Kit faster than react-markdown?
- Depends on the document. On the reference machine in benchmarks/README.md it's 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