Render Comark in React
The @comark/react package provides React components for rendering Comark content with full support for custom components, plugins, and streaming.
Installation
pnpm add @comark/reactnpm install @comark/reactyarn add @comark/reactbun add @comark/react<Comark>
The <Comark> component is the simplest way to render markdown in React. It handles parsing and rendering automatically.
<Comark> is an async component. You can also use the <ComarkRenderer> component to handle parsing yourself.import { Comark } from '@comark/react'
const content = `# Hello World
This is **markdown** with Comark components.
`
export default function App() {
return <Comark>{content}</Comark>
}Usage
Pass markdown content via children or the markdown prop:
<Comark>{content}</Comark><Comark markdown={content} />Props
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | - | Markdown content to parse and render |
markdown | string | '' | Alternative to children |
options | ParseOptions | {} | Parser options (autoUnwrap, autoClose, etc.) |
plugins | ComarkPlugin[] | [] | Array of plugins |
components | Record<string, ComponentType> | {} | Custom React component mappings |
componentsManifest | (name: string) => Promise<Component> | undefined | Dynamic component resolver |
streaming | boolean | false | Enable streaming mode |
caret | boolean | { class: string } | false | Append caret to last text node |
data | Record<string, unknown> | undefined | Runtime values referenced from markdown via :prop="data.path" |
className | string | undefined | CSS class for wrapper element |
options
See ParseOptions for available options.
<Comark options={{ autoUnwrap: true, autoClose: true }}>
{content}
</Comark>plugins
See ComarkPlugin for available plugins.
import { Comark } from '@comark/react'
import highlight from '@comark/react/plugins/highlight'
import githubLight from '@shikijs/themes/github-light'
import githubDark from '@shikijs/themes/github-dark'
const plugins = [
highlight({
themes: { light: githubLight, dark: githubDark }
})
]
export default function App() {
return <Comark plugins={plugins}>{content}</Comark>
}For math and mermaid plugins, also pass the companion components:
import { Comark } from '@comark/react'
import math, { Math } from '@comark/react/plugins/math'
import mermaid, { Mermaid } from '@comark/react/plugins/mermaid'
export default function App() {
return (
<Comark
markdown={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.
Create a component
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
import { Comark } from '@comark/react'
import Alert from './components/Alert'
import Card from './components/Card'
const components = { alert: Alert, card: Card }
export default function App() {
return <Comark components={components}>{content}</Comark>
}Use it in your Markdown
::alert{type="warning"}
This is a warning message!
::componentsManifest
For lazy-loading components on demand. Components are resolved via React.lazy() and wrapped in <Suspense> automatically. Works with both <Comark> and <ComarkRenderer>:
import { Comark } from '@comark/react'
const manifest = (name: string) => {
return import(`./components/prose/${name}.tsx`)
}
export default function App() {
return <Comark componentsManifest={manifest}>{content}</Comark>
}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 for the full scope.
import { Comark } from '@comark/react'
const user = { name: 'Ada', role: 'admin' }
const content = `Hello, :badge{:label="data.user.name"}!`
export default function App() {
return <Comark markdown={content} data={{ user }} />
}defineComarkComponent
Creates a pre-configured <Comark> component with default options, plugins, and components baked in.
Usage
Expose your configured component
import { defineComarkComponent } from '@comark/react'
import highlight from '@comark/react/plugins/highlight'
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 AppComark = defineComarkComponent({
name: 'AppComark',
plugins: [
math(),
mermaid(),
highlight({
themes: {
light: githubLight,
dark: githubDark
},
}),
],
components: {
Math,
Mermaid,
alert: CustomAlert,
},
})Use it in your templates
import { AppComark } from './comark'
export default function App() {
return (
<>
{/* All configuration is already included */}
<AppComark>{content}</AppComark>
{/* Can still override per-instance */}
<AppComark components={{ alert: DifferentAlert }}>
{content}
</AppComark>
</>
)
}Options
| Option | Type | Default | Description |
|---|---|---|---|
extends | ReturnType<typeof defineComarkComponent> | 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 |
html | boolean | true | Parse embedded HTML tags into AST nodes |
plugins | ComarkPlugin[] | [] | Array of plugins |
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:
import { defineComarkComponent } from '@comark/react'
import highlight from '@comark/react/plugins/highlight'
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 BaseComark = defineComarkComponent({
name: 'BaseComark',
plugins: [highlight({ themes: { light: githubLight, dark: githubDark } })],
components: { pre: CodeBlock },
})
// Article: extends Base, adds TOC and math
export const ArticleComark = defineComarkComponent({
name: 'ArticleComark',
extends: BaseComark,
plugins: [toc({ depth: 3 }), math()],
components: { Math },
})
// Comment: extends Base only — no TOC, no math
export const CommentComark = defineComarkComponent({
name: 'CommentComark',
extends: BaseComark,
})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
'use client'
import { defineComarkComponent } from '@comark/react'
import math from '@comark/react/plugins/math'
import { Math } from '@comark/react/plugins/math'
export const DocsComark = defineComarkComponent({
name: 'DocsComark',
plugins: [math()],
components: { Math },
})import { DocsComark } from '@/components/comark'
export default async function Page({ params }: { params: { slug: string } }) {
const content = await getDocContent(params.slug)
return <DocsComark>{content}</DocsComark>
}<ComarkRenderer>
Renders a pre-parsed ComarkTree without any parsing. Use it when you parse on the server, in a build step, or via an API — no parser or plugin code is shipped to the browser.
Parsing
Parse your markdown content and pass the tree directly in a React Server Component:
import { createParse } from 'comark'
import { readFile } from 'node:fs/promises'
import { join } from 'node:path'
import { ComarkRenderer } from '@comark/react'
import Alert from '@/components/Alert'
const parse = createParse()
export default async function DocsPage({ params }: { params: { slug: string } }) {
const markdown = await readFile(join('content', `${params.slug}.md`), 'utf-8')
const tree = await parse(markdown)
return <ComarkRenderer tree={tree} components={{ alert: Alert }} />
}Renderer Props
| Prop | Type | Default | Description |
|---|---|---|---|
tree | ComarkTree | — | Required. The parsed tree returned by parse() |
components | Record<string, ComponentType> | {} | Custom React component mappings |
componentsManifest | ComponentManifest | undefined | Dynamic component resolver for lazy-loaded components |
streaming | boolean | false | Enable streaming mode |
caret | boolean | { class: string } | false | Append a blinking caret to the last text node |
data | Record<string, unknown> | undefined | Runtime values referenced from markdown via :prop="data.path" |
className | string | undefined | CSS class for the wrapper <div> |
defineComarkRendererComponent
Creates a pre-configured <ComarkRenderer> with baked-in component mappings.
Setup
Expose your configured renderer
import { defineComarkRendererComponent } from '@comark/react'
import Alert from './components/Alert'
import CodeBlock from './components/CodeBlock'
export const ArticleRenderer = defineComarkRendererComponent({
name: 'ArticleRenderer',
components: {
alert: Alert,
pre: CodeBlock,
},
})Page integration
import { parse } from 'comark'
import { ArticleRenderer } from '@/comark'
export default async function Page({ params }: { params: { slug: string } }) {
const markdown = await getContent(params.slug)
const tree = await parse(markdown)
return <ArticleRenderer tree={tree} />
}Renderer Options
| Option | Type | Default | Description |
|---|---|---|---|
extends | ReturnType<typeof defineComarkRendererComponent> | undefined | Inherit component mappings from another renderer |
name | string | undefined | Component name for debugging |
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:
import { defineComarkRendererComponent } from '@comark/react'
import CodeBlock from './components/CodeBlock'
import ProseA from './components/ProseA'
import Alert from './components/Alert'
import CommentAlert from './components/CommentAlert'
const BaseRenderer = defineComarkRendererComponent({
name: 'BaseRenderer',
components: { pre: CodeBlock, a: ProseA },
})
export const ArticleRenderer = defineComarkRendererComponent({
name: 'ArticleRenderer',
extends: BaseRenderer,
components: { alert: Alert },
})
export const CommentRenderer = defineComarkRendererComponent({
name: 'CommentRenderer',
extends: BaseRenderer,
components: { alert: CommentAlert },
})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="foo"} | className="foo" |
{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}(e.g.,#footer→slotFooter)
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>
)
}::card{title="My Card"}
Default slot content.
#footer
Footer slot content.
::Overriding HTML Elements
Override how native HTML elements render by mapping a component to their tag name via the components prop.
Create overridden version
interface HeadingProps {
__node?: ComarkElement
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
<Comark components={{ h1: Heading, h2: Heading, h3: Heading }}>
{content}
</Comark>Resolution Order
Components are resolved in this order:
Prose{PascalTag}— e.g.,ProseH1forh1{PascalTag}— e.g.,Alertforalert{tag}— e.g.,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:
import { useState } from 'react'
import { Comark } 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 (
<Comark streaming={isStreaming} caret>
{content}
</Comark>
)
}autoClose is enabled by default — incomplete syntax like **bold text is automatically closed on every parse. Disable with options={{ autoClose: false }}.Caret
The caret prop appends a blinking cursor to the last text node while streaming is true:
{/* Default caret */}
<Comark streaming={isStreaming} caret>{content}</Comark>
{/* Custom caret class */}
<Comark streaming={isStreaming} caret={{ class: 'my-caret' }}>{content}</Comark>.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 ComarkElement to type the __node prop in components that override HTML elements:
import type { ComponentType } from 'react'
import type { ComarkPlugin } from 'comark'
import { Comark } from '@comark/react'
interface Props {
content: string
components?: Record<string, ComponentType<any>>
plugins?: ComarkPlugin[]
}
export default function ComarkWrapper({ content, components, plugins }: Props) {
return <Comark components={components} plugins={plugins}>{content}</Comark>
}import type { ComarkElement } from 'comark'
interface HeadingProps {
__node?: ComarkElement
id?: string
children: React.ReactNode
}
export default function Heading({ __node, id, children }: HeadingProps) {
const Tag = __node?.[0] || 'h2'
return <Tag id={id}>{children}</Tag>
}