Skip to main content

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>
}
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. 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.

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]} 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>
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 too. 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 the override does, the URL has already passed the default policy: javascript: and other unlisted schemes arrive 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 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>
},
}
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 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.

  1. Raw HTML is not executed. It renders as visible escaped text. skipHtml removes it instead. Neither option executes it.
  2. URL schemes are filtered. http, https, irc, ircs, mailto and xmpp pass; everything else is emptied. A colon after the first /, ? or # is part of a path, so relative URLs are untouched.
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 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