Skip to main content

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>
}
Edit the source and watch the output follow
Markdown (edit me)
Rendered

Release notes

Shipped today after a long review.

  1. Faster cold render
  2. 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.

GFM on: tables, task lists, strikethrough
Markdown (edit me)
Rendered
DocumentCold renderRatio
1 KB2.69 ms1.21x
100 KB206.96 ms0.89x
  • tables
  • task lists
  • struck text

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>
h2 and blockquote replaced by components
Markdown
## A section

> A quote rendered by a component.

Ordinary paragraph.
Rendered

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.

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.

Links, including one the policy empties
Markdown
An [ordinary link](https://example.com), a [relative one](/docs/styling),
an autolink <https://example.com/feed>, and a [hostile one](javascript:alert(1)).

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>
},
}
A fenced block keeps its info string
Markdown
Inline `code` stays inline.

```ts
const preset = defineMarkdownPreset({ extensions: [gfm()] })
```
Rendered

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.

  1. Raw HTML isn't executed. It renders as visible escaped text, or skipHtml removes it. Either way it doesn't run.
  2. URL schemes are filtered. http, https, irc, ircs, mailto and xmpp pass and everything else is emptied. A colon after the first /, ? or # counts as part of the path, so relative URLs aren't affected.
Hostile source, default settings
Markdown
Hello <img src=x onerror="alert(1)"> world.

<script>alert('nope')</script>

[Click me](javascript:alert(1))
Rendered

Hello <img src=x onerror="alert(1)"> world.

<script>alert('nope')</script>

Click me

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