How to render Markdown in React
Every example on this page is rendered by @react-markdown-kit/renderer
itself, server-rendered with the page, so the output is in the HTML before any
JavaScript runs. The left pane is the source, the right pane is the output.
npm install @react-markdown-kit/renderer
Render a Markdown string
Pass the string as children. That is the whole first-use experience: no provider, no stylesheet, no configuration.
import Markdown from '@react-markdown-kit/renderer'
export function Post({ content }: { content: string }) {
return <Markdown>{content}</Markdown>
}
Release notes
Shipped today after a long review.
- Faster cold render
- Smaller payload
Upgrade at your leisure.
The output is plain semantic HTML: h2, p, strong, ol, blockquote. No
classes, no inline styles, nothing to override. Style it with your own CSS, or
opt into the shipped typography: four ways to style
it.
The parser is micromark, not regular expressions. 554 of the 652 official
CommonMark examples match byte for byte, and 96% match once the raw-HTML
examples the security policy drops on purpose are counted separately
(tests/commonmark.test.ts).
Render tables and task lists with GFM
Tables, task lists, strikethrough, autolinks and footnotes are GitHub Flavored Markdown, so they are off until you ask for them.
import Markdown, { defineMarkdownPreset, gfm } from '@react-markdown-kit/renderer'
const preset = defineMarkdownPreset({ extensions: [gfm()] })
export function Post({ content }: { content: string }) {
return <Markdown preset={preset}>{content}</Markdown>
}
Build the preset once at module scope. Building it inside the component creates a new preset on every render.
| Document | Cold render | Ratio |
|---|---|---|
| 1 KB | 2.69 ms | 1.21x |
| 100 KB | 206.96 ms | 0.89x |
- tables
- task lists
-
strucktext
The numbers in this table are the real benchmark results, from
benchmarks/README.md. Ratios
are kit divided by baseline, so 0.89x is faster than react-markdown@10.1.0
and 1.21x is slower.
remarkPlugins={[remarkGfm]} works too, and is fixture-tested to produce the
same output as the native extension
(tests/gfm.test.ts).
GFM reference.
Replace elements with your own components
Map an element name to a component. The component receives the usual props plus the source node.
import Markdown from '@react-markdown-kit/renderer'
import { Callout } from './Callout'
const components = {
h2: (props) => <h2 className="section-heading" {...props} />,
blockquote: Callout,
}
<Markdown components={components}>{content}</Markdown>
## A section > A quote rendered by a component. Ordinary paragraph.
A section
Ordinary paragraph.
Define the components object outside the component too. A new object on every
render remounts every overridden element.
Components reference.
Links
Markdown links become <a>. To route them through your framework's link
component, or to mark external links, override a.
import NextLink from 'next/link'
const components = {
a: ({ href = '', children, ...rest }) =>
href.startsWith('/') ? (
<NextLink href={href} {...rest}>{children}</NextLink>
) : (
<a href={href} target="_blank" rel="noreferrer noopener" {...rest}>{children}</a>
),
}
Whatever the override does, the URL has already passed the default policy:
javascript: and other unlisted schemes arrive as an empty string.
An [ordinary link](https://example.com), a [relative one](/docs/styling), an autolink <https://example.com/feed>, and a [hostile one](javascript:alert(1)).
An ordinary link, a relative one, an autolink https://example.com/feed, and a hostile one.
The last link renders with nothing to navigate to. The rule is
defaultUrlTransform, which is exported so an override can call it.
Code blocks
A fenced block renders as <pre><code class="language-ts">. Override code to
add highlighting, a copy button or a filename bar.
const components = {
code: ({ className = '', children, ...rest }) => {
const language = /language-(\w+)/.exec(className)?.[1]
return <code className={className} data-language={language} {...rest}>{children}</code>
},
}
Inline `code` stays inline.
```ts
const preset = defineMarkdownPreset({ extensions: [gfm()] })
```Inline code stays inline.
const preset = defineMarkdownPreset({ extensions: [gfm()] })
The info string after the opening fence becomes class="language-ts", which
is the hook every highlighter expects.
Highlighting is not built in. Bringing your own highlighter keeps it out of the bundle of every page that renders Markdown without code.
Render Markdown you did not write
Two defaults do the work, and both are asserted in
packages/renderer/tests/render.dom.test.tsx.
- Raw HTML is not executed. It renders as visible escaped text.
skipHtmlremoves it instead. Neither option executes it. - URL schemes are filtered.
http,https,irc,ircs,mailtoandxmpppass; everything else is emptied. A colon after the first/,?or#is part of a path, so relative URLs are untouched.
Hello <img src=x onerror="alert(1)"> world.
<script>alert('nope')</script>
[Click me](javascript:alert(1))Hello <img src=x onerror="alert(1)"> world.
<script>alert('nope')</script>The tags are text. The script does not run. The link has no destination. The same policy runs on a precompiled document, so compiling is not a bypass (test).
Narrow the output further with allowedElements, disallowedElements or
allowElement. If a document genuinely needs HTML, opt in with rehype-raw
and rehype-sanitize in that order.
Security model.
Render Markdown on the server
The renderer entry has no use client directive and touches no browser global,
so the same component works in a React server component, during SSR and in a
static build. No JavaScript ships for the Markdown itself.
// app/posts/[slug]/page.tsx
import { readFile } from 'node:fs/promises'
import Markdown from '@react-markdown-kit/renderer'
export default async function Page({ params }) {
const content = await readFile(`content/${params.slug}.md`, 'utf8')
return <Markdown>{content}</Markdown>
}
Parse once for a document that does not change. compileMarkdown returns plain
JSON that survives a cache, a queue or an HTTP boundary, and re-rendering from
it is about 2.8 times faster in the benchmark
(results).
import Markdown, { compileMarkdown } from '@react-markdown-kit/renderer'
const TERMS = compileMarkdown(await readFile('content/terms.md', 'utf8'))
export default function Page() {
return <Markdown document={TERMS} />
}
Server rendering · Markdown in Next.js
Render Markdown that is still being written
If the string grows one token at a time, as it does behind a model, hand the renderer the longer string each time. Every prefix renders, and the finished blocks above stay byte-identical (streaming tests). Streaming Markdown in React.
FAQ
- How do I render Markdown in React?
- Install @react-markdown-kit/renderer and pass the Markdown string as children of the Markdown component. There is no provider, no stylesheet and no configuration, and the output is plain semantic HTML with no classes.
- How do I render Markdown tables and task lists in React?
- Tables, task lists, strikethrough, autolinks and footnotes are GitHub Flavored Markdown. Turn them on with defineMarkdownPreset({ extensions: [gfm()] }) and pass the preset, or pass remark-gfm in remarkPlugins if you already use it.
- Is it safe to render Markdown from users in React?
- Raw HTML is not executed by default and is rendered as visible escaped text, and URL schemes outside http, https, irc, ircs, mailto and xmpp are emptied. Both defaults are asserted in packages/renderer/tests/render.dom.test.tsx. Plugins and components you pass are your own trusted code.
- Can I render Markdown in a React server component?
- Yes. The renderer entry carries no use client directive and touches no browser global, so it renders in a server component, during SSR and in a static build, shipping no JavaScript for the Markdown itself.
Next
Renderer demo · @react-markdown-kit/renderer on npm · React Markdown renderer · Compatibility with react-markdown · Migrate from react-markdown