How to render Markdown in React
Every example on this page is rendered by @react-markdown-kit/renderer
itself and server-rendered with the page, so the output is already in the HTML
before any JavaScript runs. The left pane is the source and the right pane is
the output.
npm install @react-markdown-kit/renderer
Render a Markdown string
Pass the string as children. You don't need a provider, a stylesheet or any configuration to get started.
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)
without classes or inline styles, so there's nothing to override. Style it with
your own CSS, or opt into the typography that ships with the package: four ways to style
it.
Parsing is done by micromark (no 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 part of GitHub Flavored Markdown, so they're off until you turn them on.
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. If you build it inside the component you'll get 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]} also works, and there are fixture tests checking it
produces 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 gets 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 as well. 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 your override does, the URL has already gone through the default
policy, so javascript: and other unlisted schemes come in 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 lives in
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 what highlighters generally look for.
Highlighting isn't built in. If you bring your own highlighter, pages that render Markdown without code don't have to load it.
Render Markdown you did not write
Two defaults handle this, and both are tested in
packages/renderer/tests/render.dom.test.tsx.
- Raw HTML isn't executed. It renders as visible escaped text, or
skipHtmlremoves it. Either way it doesn't run. - URL schemes are filtered.
http,https,irc,ircs,mailtoandxmpppass and everything else is emptied. A colon after the first/,?or#counts as part of the path, so relative URLs aren't affected.
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 show up as text, the script doesn't run and the link has no destination. The same policy runs on a precompiled document, so compiling first doesn't get around it (test).
Narrow the output further with allowedElements, disallowedElements or
allowElement. If a document really 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 doesn't touch any browser
globals, 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>
}
For a document that doesn't change, you can parse it once. 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 (like output from a model), pass the renderer the longer string each time. Every prefix renders, and the blocks that are already finished 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. You don't need a provider, a stylesheet or any 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 part of 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?
- By default raw HTML isn't executed (it shows up as escaped text), and URL schemes other than http, https, irc, ircs, mailto and xmpp are emptied. Both defaults are tested in packages/renderer/tests/render.dom.test.tsx. Plugins and components you pass in are treated as your own trusted code.
- Can I render Markdown in a React server component?
- Yes. The renderer entry has no use client directive and doesn't touch any browser globals, so it renders in a server component, during SSR and in a static build, and no JavaScript ships for the Markdown itself.
Next
Renderer demo · @react-markdown-kit/renderer on npm · React Markdown renderer · Compatibility with react-markdown · Migrate from react-markdown