---
title: "Render Comark in React"
description: "Learn how to render Comark in a React application with custom components, plugins, and Next.js support."
canonical_url: "https://comark.dev/rendering/react"
---
# Render Comark in React

> Learn how to render Comark in a React application with custom components, plugins, and Next.js support.

The `@comark/react` package provides React components for rendering Comark content with full support for custom components, plugins, and streaming.

## Installation

<code-group>
```bash [pnpm]
pnpm add @comark/react
```


```bash [npm]
npm install @comark/react
```


```bash [yarn]
yarn add @comark/react
```


```bash [bun]
bun add @comark/react
```
</code-group>

## `<Markdown>`

The `<Markdown>` component is the quickest way to render markdown in React. It handles parsing and rendering automatically.

<warning to="#code-markdowndocument">
`<Markdown>` is an **async** component. You can also use the `<MarkdownDocument>` component to handle parsing yourself.
</warning>

~~~tsx [App.tsx]
import { Markdown } from '@comark/react'

const content = `# Hello World

This is **markdown** with Comark components.
`

export default function App() {
  return <Markdown>{content}</Markdown>
}
~~~

### Usage

Pass markdown content via `children` or the `value` prop. `value` accepts a markdown **string** or a pre-parsed [`MarkdownDocument`](https://comark.dev/getting-started/document-model):

<code-group>
```tsx [Children]
<Markdown>{content}</Markdown>
```


```tsx [String]
<Markdown value={content} />
```


```tsx [MarkdownDocument]
import type { MarkdownDocument } from 'comark'

export default function Article({ document }: { document: MarkdownDocument }) {
  return <Markdown value={document} />
}
```
</code-group>

<callout icon="i-lucide-package" color="warning">
Passing a document to `<Markdown>` skips parsing at runtime, but the **parser is still bundled** because `Markdown` imports it. To keep the client bundle free of the parser, use [`<MarkdownDocument>`](#code-markdowndocument) instead.
</callout>

### Props

| Prop                                                                 | Type                                   | Default     | Description                                                                                                                                      |
| -------------------------------------------------------------------- | -------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `children`                                                           | `React.ReactNode`                      | -           | Markdown content to parse and render                                                                                                             |
| `value`                                                              | `string \| MarkdownDocument`           | `''`        | Markdown string or pre-parsed document (alternative to children)                                                                                 |
| [`options`](#code-markdown-props-code-options)                       | `ParserOptions`                        | `{}`        | Parser options (autoUnwrap, autoClose, etc.)                                                                                                     |
| [`plugins`](#code-markdown-props-code-plugins)                       | `ComarkPlugin[]`                       | `[]`        | Array of plugins                                                                                                                                 |
| `unwrap`                                                             | `boolean \| string \| string[]`        | `false`     | Strip wrapper tags (MDC `unwrap`) — `true` unwraps `<p>`; a comma/space-separated string or array peels tags sequentially, for example `"ul li"` |
| [`components`](#code-markdown-props-code-components)                 | `Record<string, ComponentType>`        | `{}`        | Custom React component mappings                                                                                                                  |
| [`componentsManifest`](#code-markdown-props-code-componentsmanifest) | `(name: string) => Promise<Component>` | `undefined` | Dynamic component resolver                                                                                                                       |
| [`streaming`](#streaming)                                            | `boolean`                              | `false`     | Enable streaming mode                                                                                                                            |
| [`caret`](#streaming-caret)                                          | `boolean \| { class: string }`         | `false`     | Append caret to last text node                                                                                                                   |
| [`data`](#code-markdown-props-code-data)                             | `Record<string, unknown>`              | `undefined` | Runtime values referenced from markdown via `:prop="data.path"`                                                                                  |
| `className`                                                          | `string`                               | `undefined` | CSS class for wrapper element                                                                                                                    |

#### `options`

See [ParserOptions](https://github.com/comarkdown/comark/blob/main/packages/comark/src/types.ts#L247) for available options.

```tsx [App.tsx]
<Markdown options={{ autoUnwrap: true, autoClose: true }}>
  {content}
</Markdown>
```

#### `plugins`

See [ComarkPlugin](https://comark.dev/plugins) for available plugins.

```tsx [App.tsx]
import { Markdown } from '@comark/react'
import shiki from '@comark/react/plugins/shiki'
import githubLight from '@shikijs/themes/github-light'
import githubDark from '@shikijs/themes/github-dark'

const plugins = [
  shiki({
    themes: { light: githubLight, dark: githubDark }
  })
]

export default function App() {
  return <Markdown plugins={plugins}>{content}</Markdown>
}
```

For math and mermaid plugins, also pass the companion components:

```tsx [App.tsx]
import { Markdown } from '@comark/react'
import math, { Math } from '@comark/react/plugins/math'
import mermaid, { Mermaid } from '@comark/react/plugins/mermaid'
import 'katex/dist/katex.min.css'

export default function App() {
  return (
    <Markdown
      value={markdown}
      components={{ Math, Mermaid }}
      plugins={[math(), mermaid()]}
    />
  )
}
```

#### `components`

Use this prop to map custom React components to Comark elements and use them in your markdown.

<steps level="4">
#### Create a component


```tsx [components/Alert.tsx]
interface AlertProps {
  type?: 'info' | 'warning' | 'error' | 'success'
  children: React.ReactNode
}

export default function Alert({ type = 'info', children }: AlertProps) {
  return (
    <div className={`alert alert-${type}`} role="alert">
      {children}
    </div>
  )
}
```


#### Map the tag to your component


```tsx [App.tsx]
import { Markdown } from '@comark/react'
import Alert from './components/Alert'
import Card from './components/Card'

const components = { alert: Alert, card: Card }

export default function App() {
  return <Markdown components={components}>{content}</Markdown>
}
```


#### Use it in your Markdown


```mdc
::alert{type="warning"}
This is a warning message!
::
```
</steps>

<tip>
See [Component Bindings](#component-bindings) for how props and slots map to your React component, or [Component Syntax](https://comark.dev/syntax/components) for the full Comark syntax API: nested components, inline syntax, and more.
</tip>

#### `componentsManifest`

For lazy-loading components on demand. Components are resolved via `React.lazy()` and wrapped in `<Suspense>` automatically. Works with both `<Markdown>` and `<MarkdownDocument>`:

```tsx [App.tsx]
import { Markdown } from '@comark/react'

const manifest = (name: string) => {
  return import(`./components/prose/${name}.tsx`)
}

export default function App() {
  return <Markdown componentsManifest={manifest}>{content}</Markdown>
}
```

#### `data`

Expose runtime values to markdown authors. Any prop written with a `:` prefix is resolved against the render context `{ frontmatter, meta, data, props }` when its value isn't valid JSON. See [Data Binding](https://comark.dev/syntax/components#data-binding) for the full scope.

```tsx [App.tsx]
import { Markdown } from '@comark/react'

const user = { name: 'Ada', role: 'admin' }
const content = `Hello, :badge{:label="data.user.name"}!`

export default function App() {
  return <Markdown value={content} data={{ user }} />
}
```

### `defineMarkdownComponent`

Creates a pre-configured `<Markdown>` component with default options, plugins, and components baked in.

#### Usage

<steps level="4">
#### Expose your configured component


```typescript [markdown.ts]
import { defineMarkdownComponent } from '@comark/react'
import shiki from '@comark/react/plugins/shiki'
import math, { Math } from '@comark/react/plugins/math'
import mermaid, { Mermaid } from '@comark/react/plugins/mermaid'
import githubLight from '@shikijs/themes/github-light'
import githubDark from '@shikijs/themes/github-dark'
import CustomAlert from './components/CustomAlert'

export const AppMarkdown = defineMarkdownComponent({
  name: 'AppMarkdown',

  plugins: [
    math(),
    mermaid(),
    shiki({
      themes: {
        light: githubLight,
        dark: githubDark
      },
    }),
  ],

  components: {
    Math,
    Mermaid,
    alert: CustomAlert,
  },
})
```


#### Use it in your templates


```tsx [App.tsx]
import { AppMarkdown } from './markdown'

export default function App() {
  return (
    <>
      {/* All configuration is already included */}
      <AppMarkdown>{content}</AppMarkdown>

      {/* Can still override per-instance */}
      <AppMarkdown components={{ alert: DifferentAlert }}>
        {content}
      </AppMarkdown>
    </>
  )
}
```
</steps>

#### Options

| Option                                                                | Type                                         | Default     | Description                                                                                        |
| --------------------------------------------------------------------- | -------------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------- |
| [`extends`](#code-markdown-code-definemarkdowncomponent-code-extends) | `ReturnType<typeof defineMarkdownComponent>` | `undefined` | Inherit plugins and components from another component                                              |
| `name`                                                                | `string`                                     | `undefined` | Component name for debugging                                                                       |
| `autoUnwrap`                                                          | `boolean`                                    | `true`      | Automatically unwrap single block elements                                                         |
| `autoClose`                                                           | `boolean`                                    | `true`      | Auto-close incomplete markdown syntax                                                              |
| `linkify`                                                             | `boolean`                                    | `true`      | Auto-convert URL-like text into links                                                              |
| `registerDefaultPlugins`                                              | `boolean`                                    | `true`      | Register default plugins (`frontmatter`, `html`, `alert`, `task-list`, `components`, `attributes`) |
| [`plugins`](#code-markdown-props-code-plugins)                        | `ComarkPlugin[]`                             | `[]`        | Array of plugins                                                                                   |
| [`components`](#code-markdown-props-code-components)                  | `Record<string, ComponentType>`              | `{}`        | Custom React component mappings                                                                    |
| `className`                                                           | `string`                                     | `undefined` | Additional CSS classes for the wrapper div                                                         |

#### `extends`

Inherit plugins and components from another component, then layer your own on top:

```typescript [markdown/index.ts]
import { defineMarkdownComponent } from '@comark/react'
import shiki from '@comark/react/plugins/shiki'
import githubLight from '@shikijs/themes/github-light'
import githubDark from '@shikijs/themes/github-dark'
import toc from '@comark/react/plugins/toc'
import math, { Math } from '@comark/react/plugins/math'
import CodeBlock from './components/CodeBlock'

// Base: highlight + shared prose overrides, used everywhere
const BaseMarkdown = defineMarkdownComponent({
  name: 'BaseMarkdown',
  plugins: [shiki({ themes: { light: githubLight, dark: githubDark } })],
  components: { pre: CodeBlock },
})

// Article: extends Base, adds TOC and math
export const ArticleMarkdown = defineMarkdownComponent({
  name: 'ArticleMarkdown',
  extends: BaseMarkdown,
  plugins: [toc({ depth: 3 }), math()],
  components: { Math },
})

// Comment: extends Base only, no TOC, no math
export const CommentMarkdown = defineMarkdownComponent({
  name: 'CommentMarkdown',
  extends: BaseMarkdown,
})
```

#### Merging behavior

- `plugins`: Arrays are concatenated (config plugins + prop plugins)
- `components`: Props override config (prop components take precedence)
- Other `options`: Props override config

#### Usage with Next.js App Router

```tsx [markdown.ts]
'use client'

import { defineMarkdownComponent } from '@comark/react'
import math, { Math } from '@comark/react/plugins/math'

export const DocsMarkdown = defineMarkdownComponent({
  name: 'DocsMarkdown',
  plugins: [math()],
  components: { Math },
})
```

```tsx [app/docs/[slug\\]/page.tsx]
import { DocsMarkdown } from '@/components/markdown'

export default async function Page({ params }: { params: { slug: string } }) {
  const content = await getDocContent(params.slug)
  return <DocsMarkdown>{content}</DocsMarkdown>
}
```

---

## `<MarkdownDocument>`

Renders a pre-parsed `MarkdownDocument` without any parsing. Use it when you parse on the server, in a build step, or via an API, so no parser or plugin code is shipped to the browser.

### Parsing

Parse your markdown content and pass the document directly in a React Server Component:

```tsx [app/docs/[slug\\]/page.tsx]
import { createMarkdownParser } from 'comark'
import { readFile } from 'node:fs/promises'
import { join } from 'node:path'
import { MarkdownDocument } from '@comark/react'
import Alert from '@/components/Alert'

const parse = createMarkdownParser()

export default async function DocsPage({ params }: { params: { slug: string } }) {
  const markdown = await readFile(join('content', `${params.slug}.md`), 'utf-8')
  const document = await parseMarkdown(markdown)

  return <MarkdownDocument value={document} components={{ alert: Alert }} />
}
```

### Renderer props

| Prop                                                                 | Type                            | Default     | Description                                                     |
| -------------------------------------------------------------------- | ------------------------------- | ----------- | --------------------------------------------------------------- |
| `value`                                                              | `MarkdownDocument`              | —           | **Required.** The parsed document returned by `parseMarkdown()` |
| [`components`](#code-markdown-props-code-components)                 | `Record<string, ComponentType>` | `{}`        | Custom React component mappings                                 |
| [`componentsManifest`](#code-markdown-props-code-componentsmanifest) | `ComponentManifest`             | `undefined` | Dynamic component resolver for lazy-loaded components           |
| [`streaming`](#streaming)                                            | `boolean`                       | `false`     | Enable streaming mode                                           |
| [`caret`](#streaming-caret)                                          | `boolean \| { class: string }`  | `false`     | Append a blinking caret to the last text node                   |
| [`data`](#code-markdown-props-code-data)                             | `Record<string, unknown>`       | `undefined` | Runtime values referenced from markdown via `:prop="data.path"` |
| `className`                                                          | `string`                        | `undefined` | CSS class for the wrapper `<div>`                               |

### `defineMarkdownDocumentComponent`

Creates a pre-configured `<MarkdownDocument>` with baked-in component mappings.

#### Setup

<steps level="4">
#### Expose your configured renderer


```tsx [markdown/index.ts]
import { defineMarkdownDocumentComponent } from '@comark/react'
import Alert from './components/Alert'
import CodeBlock from './components/CodeBlock'

export const ArticleMarkdownDocument = defineMarkdownDocumentComponent({
  name: 'ArticleMarkdownDocument',
  components: {
    alert: Alert,
    pre: CodeBlock,
  },
})
```


#### Page integration


```tsx [app/docs/[slug\\]/page.tsx]
import { parseMarkdown } from 'comark'
import { ArticleMarkdownDocument } from '@/markdown'

export default async function Page({ params }: { params: { slug: string } }) {
  const markdown = await getContent(params.slug)
  const document = await parseMarkdown(markdown)

  return <ArticleMarkdownDocument value={document} />
}
```
</steps>

#### Renderer options

| Option                                                                               | Type                                                 | Default     | Description                                      |
| ------------------------------------------------------------------------------------ | ---------------------------------------------------- | ----------- | ------------------------------------------------ |
| [`extends`](#code-markdowndocument-code-definemarkdowndocumentcomponent-inheritance) | `ReturnType<typeof defineMarkdownDocumentComponent>` | `undefined` | Inherit component mappings from another renderer |
| `name`                                                                               | `string`                                             | `undefined` | Component name for debugging                     |
| [`components`](#code-markdown-props-code-components)                                 | `Record<string, ComponentType>`                      | `{}`        | Custom React component mappings                  |
| `className`                                                                          | `string`                                             | `undefined` | Additional CSS classes for the wrapper div       |

#### Inheritance

Inherit component mappings from another renderer, then layer your own on top:

```tsx [markdown/index.ts]
import { defineMarkdownDocumentComponent } from '@comark/react'
import CodeBlock from './components/CodeBlock'
import ProseA from './components/ProseA'
import Alert from './components/Alert'
import CommentAlert from './components/CommentAlert'

const BaseMarkdownDocument = defineMarkdownDocumentComponent({
  name: 'BaseMarkdownDocument',
  components: { pre: CodeBlock, a: ProseA },
})

export const ArticleMarkdownDocument = defineMarkdownDocumentComponent({
  name: 'ArticleMarkdownDocument',
  extends: BaseMarkdownDocument,
  components: { alert: Alert },
})

export const CommentMarkdownDocument = defineMarkdownDocumentComponent({
  name: 'CommentMarkdownDocument',
  extends: BaseMarkdownDocument,
  components: { alert: CommentAlert },
})
```

---

## Live documents

`MarkdownLive` is a client wrapper around `MarkdownDocument` that subscribes to an ambient **context** so external sources can drive a mounted renderer: an HMR signal, a collaboration socket, an agent editing the document while you chat with it, or devtools. It accepts the same props as `MarkdownDocument` plus a `documentKey`. The listen key is the document's own `meta.key` (set by a plugin) or the `documentKey` prop; if `globalThis.comarkContext` exists the renderer listens for updates on that key and re-renders, cleaning up on unmount. `MarkdownDocument` itself stays free of client-only hooks so it can render in a React Server Component. With no context, the key is ignored at zero cost.

```tsx
import { MarkdownLive } from '@comark/react'

<MarkdownLive documentKey="page" value={document} />
```

A **driver** installs the context once and pushes updates by key with `set()` (replace the whole document) or `patch()` (surgical node edits, with structural sharing so only the changed branch re-renders):

```ts
import { createComarkContext, parseMarkdown } from 'comark'

const ctx = createComarkContext() // installs globalThis.comarkContext
const doc = ctx.get('page', await parseMarkdown('# Hello')) // seed on first access

doc.set(await parseMarkdown('# Replaced'))
doc.patch({ op: 'insert', path: [1], node: ['p', {}, 'inserted'] })
```

A `path` is a node-index path into `document.nodes`: the first segment indexes the top-level nodes, each later segment indexes into that element's children. Patch operations are `replace`, `insert`, `remove` (each takes a `path`), plus `meta`, `frontmatter`, and `data` merges. The same context API powers `@comark/vue`, `@comark/svelte`, and `@comark/angular`: websocket handlers, agents, devtools, and HMR all drive it the same way.

---

## Component bindings

Comark automatically bridges the gap between Comark syntax and your component's interface.

### Prop binding

Attributes in Comark syntax are passed as props to your component. Use the `:` prefix to pass typed values. HTML attribute names are automatically converted to their React equivalents:

| Markdown                    | React prop                         |
| --------------------------- | ---------------------------------- |
| `{type="warning"}`          | `type="warning"` (string)          |
| `{:count="5"}`              | `count={5}` (number)               |
| `{:active="true"}`          | `active={true}` (boolean)          |
| `{:config='{"key":"val"}'}` | `config={{ key: 'val' }}` (object) |
| `{class="highlight"}`       | `className="highlight"`            |
| `{tabindex="0"}`            | `tabIndex={0}`                     |
| `{style="color: red"}`      | `style={{ color: 'red' }}`         |

### Named slots

Named slots in Comark (`#slotname`) map to `slot{Name}` props in React:

- **Default slot** → `children`
- **Named slots** → `slot{Name}` (for example, `#footer` → `slotFooter`)

<code-group>
```tsx [React]
interface CardProps {
  title?: string
  children?: React.ReactNode
  slotFooter?: React.ReactNode
}

export default function Card({ title, children, slotFooter }: CardProps) {
  return (
    <div className="card">
      {title && <h3>{title}</h3>}
      <div className="card-body">{children}</div>
      {slotFooter && <div className="card-footer">{slotFooter}</div>}
    </div>
  )
}
```


```mdc [Comark]
::card{title="My Card"}
Default slot content.

#footer
Footer slot content.
::
```
</code-group>

---

## Overriding HTML elements

Override how native HTML elements render by mapping a component to their tag name via the `components` prop.

<steps level="3">
### Create overridden version


```tsx [components/Heading.tsx]
interface HeadingProps {
  __node?: ElementNode
  id?: string
  children: React.ReactNode
}

export default function Heading({ __node, id, children }: HeadingProps) {
  const Tag = __node?.[0] || 'h2'
  return (
    <Tag id={id} className="heading">
      {id && <a href={`#${id}`} className="anchor">#</a>}
      {children}
    </Tag>
  )
}
```


### Map


```tsx [App.tsx]
<Markdown components={{ h1: Heading, h2: Heading, h3: Heading }}>
  {content}
</Markdown>
```
</steps>

### Resolution order

Components are resolved in this order:

1. **`Prose{PascalTag}`**: for example, `ProseH1` for `h1`
2. **`{PascalTag}`**: for example, `Alert` for `alert`
3. **`{tag}`**: for example, `alert`

If no custom component matches, the tag renders as a native HTML element.

---

## Streaming

Enable real-time rendering as content arrives, ideal for AI chat interfaces and live previews.

### Setup

Set `streaming` to `true` while content is being received, then `false` when done:

```tsx [components/AiChat.tsx]
import { useState } from 'react'
import { Markdown } from '@comark/react'

export default function AiChat() {
  const [content, setContent] = useState('')
  const [isStreaming, setIsStreaming] = useState(false)

  async function askAI(prompt: string) {
    setContent('')
    setIsStreaming(true)

    const response = await fetch('/api/chat', {
      method: 'POST',
      body: JSON.stringify({ prompt }),
    })

    const reader = response.body!.getReader()
    const decoder = new TextDecoder()
    let accumulated = ''

    while (true) {
      const { done, value } = await reader.read()
      if (done) break
      accumulated += decoder.decode(value, { stream: true })
      setContent(accumulated)
    }

    setIsStreaming(false)
  }

  return (
    <Markdown streaming={isStreaming} caret>
      {content}
    </Markdown>
  )
}
```

<callout icon="i-lucide-info" color="info">
`autoClose` is enabled by default: incomplete syntax like `**bold text` is automatically closed on every parse. Disable with `options={{ autoClose: false }}`.
</callout>

### Caret

The `caret` prop appends a blinking cursor to the last text node while `streaming` is `true`:

```tsx [App.tsx]
{/* Default caret */}
<Markdown streaming={isStreaming} caret>{content}</Markdown>

{/* Custom caret class */}
<Markdown streaming={isStreaming} caret={{ class: 'my-caret' }}>{content}</Markdown>
```

```css
.my-caret {
  display: inline-block;
  width: 2px;
  height: 1em;
  background: currentColor;
  animation: blink 1s step-end infinite;
  vertical-align: text-bottom;
}

@keyframes blink {
  50% { opacity: 0; }
}
```

---

## TypeScript support

Use `ComarkPlugin` from `comark` to type plugin arrays, and `ElementNode` to type the `__node` prop in components that override HTML elements:

```tsx [ComarkWrapper.tsx]
import type { ComponentType } from 'react'
import type { ComarkPlugin } from 'comark'
import { Markdown } from '@comark/react'

interface Props {
  content: string
  components?: Record<string, ComponentType<any>>
  plugins?: ComarkPlugin[]
}

export default function ComarkWrapper({ content, components, plugins }: Props) {
  return <Markdown components={components} plugins={plugins}>{content}</Markdown>
}
```

```tsx [components/Heading.tsx]
import type { ElementNode } from 'comark'

interface HeadingProps {
  __node?: ElementNode
  id?: string
  children: React.ReactNode
}

export default function Heading({ __node, id, children }: HeadingProps) {
  const Tag = __node?.[0] || 'h2'
  return <Tag id={id}>{children}</Tag>
}
```

---

---

- [Plugins](https://comark.dev/plugins)
- [Streaming API](https://comark.dev/reference/auto-close)


## Sitemap

See the full [sitemap](https://comark.dev/sitemap.md) for all pages.
