React
Render MDC content in React applications with two components: MDC for simple use cases and MDCRenderer for advanced control.
Installation
npm install mdc-syntax
import { MDC, MDCRenderer } from 'mdc-syntax/react'
MDC Component
The MDC component is the simplest way to render markdown. Pass markdown content directly and it handles parsing and rendering automatically.
Basic Usage
import { MDC } from 'mdc-syntax/react'
const content = `# Hello World
This is **markdown** with MDC components.
::alert{type="info"}
This is an alert!
::
`
export default function App() {
return <MDC markdown={content} />
}
Props
| Prop | Type | Description |
|---|---|---|
markdown | string | Markdown content to parse and render |
components | Record<string, ComponentType> | Custom component mappings |
excerpt | boolean | Only render content before <!-- more --> |
className | string | CSS class for wrapper element |
With Custom Components
import { MDC } from 'mdc-syntax/react'
import CustomAlert from './components/Alert'
import CustomCard from './components/Card'
const components = {
alert: CustomAlert,
card: CustomCard,
}
const content = `
::alert{type="warning"}
Important message here
::
::card{title="My Card"}
Card content
::
`
export default function App() {
return <MDC markdown={content} components={components} />
}
Live Editor Example
import { useState } from 'react'
import { MDC } from 'mdc-syntax/react'
export default function MarkdownEditor() {
const [content, setContent] = useState('# Edit me!\n\nType **markdown** here.')
return (
<div className="editor">
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
/>
<div className="preview">
<MDC markdown={content} />
</div>
</div>
)
}
Excerpt Mode
Render only content before <!-- more -->:
import { MDC } from 'mdc-syntax/react'
const content = `# Article Title
This is the excerpt shown in listings.
<!-- more -->
This is the full article content.
`
export default function ArticleCard() {
// Only renders "This is the excerpt shown in listings."
return <MDC markdown={content} excerpt />
}
With Tailwind CSS
<MDC
value={content}
className="prose prose-slate lg:prose-xl dark:prose-invert"
/>
MDCRenderer Component
The MDCRenderer component renders a pre-parsed Minimark AST. Use this when you need more control over parsing, want to cache parsed results, or are working with streams.
Basic Usage
import { parse } from 'mdc-syntax'
import { MDCRenderer } from 'mdc-syntax/react'
const content = `# Hello World
This is **markdown** with MDC components.
`
const result = parse(content)
export default function App() {
return <MDCRenderer body={result.body} />
}
Props
| Prop | Type | Description |
|---|---|---|
body | MinimarkTree | Required. The parsed AST to render |
components | Record<string, ComponentType> | Custom component mappings |
componentsManifest | `(name: string) => Promise | null` |
stream | boolean | Enable streaming optimizations |
className | string | CSS class for wrapper element |
With Custom Components
import { parse } from 'mdc-syntax'
import { MDCRenderer } from 'mdc-syntax/react'
import CustomAlert from './components/Alert'
const components = {
alert: CustomAlert,
h1: CustomHeading,
h2: CustomHeading,
}
const result = parse(content)
export default function App() {
return <MDCRenderer body={result.body} components={components} />
}
Streaming Support
MDCRenderer works with parseStreamIncremental for real-time content:
import { useState, useEffect } from 'react'
import { parseStreamIncremental } from 'mdc-syntax/stream'
import { MDCRenderer } from 'mdc-syntax/react'
export default function StreamingContent() {
const [body, setBody] = useState({ type: 'minimark', value: [] })
const [isComplete, setIsComplete] = useState(false)
useEffect(() => {
async function loadContent() {
const response = await fetch('/api/content')
for await (const result of parseStreamIncremental(response.body!)) {
setBody(result.body)
if (result.isComplete) {
setIsComplete(true)
}
}
}
loadContent()
}, [])
return (
<div>
<MDCRenderer body={body} stream={!isComplete} />
{!isComplete && <div className="loading">Streaming...</div>}
</div>
)
}
AI Chat Example
import { useState } from 'react'
import { parseStreamIncremental } from 'mdc-syntax/stream'
import { MDCRenderer } from 'mdc-syntax/react'
export default function Chat() {
const [messages, setMessages] = useState([])
const [currentBody, setCurrentBody] = useState({ type: 'minimark', value: [] })
const [isStreaming, setIsStreaming] = useState(false)
async function sendMessage(text: string) {
setIsStreaming(true)
const response = await fetch('/api/chat', {
method: 'POST',
body: JSON.stringify({ message: text }),
})
for await (const result of parseStreamIncremental(response.body!)) {
setCurrentBody(result.body)
if (result.isComplete) {
setMessages(prev => [...prev, result.body])
setCurrentBody({ type: 'minimark', value: [] })
setIsStreaming(false)
}
}
}
return (
<div>
{messages.map((body, i) => (
<MDCRenderer key={i} body={body} />
))}
{isStreaming && <MDCRenderer body={currentBody} stream />}
</div>
)
}
Dynamic Component Resolution
For large applications, use componentsManifest for lazy loading:
import { Suspense } from 'react'
import { MDCRenderer } from 'mdc-syntax/react'
const componentMap: Record<string, () => Promise<any>> = {
alert: () => import('./components/Alert'),
card: () => import('./components/Card'),
tabs: () => import('./components/Tabs'),
}
async function loadComponent(name: string) {
const loader = componentMap[name]
return loader ? loader() : { default: ({ children }) => <div>{children}</div> }
}
export default function App({ mdcAst }) {
return (
<Suspense fallback={<div>Loading...</div>}>
<MDCRenderer body={mdcAst} componentsManifest={loadComponent} />
</Suspense>
)
}
Custom Components
Both MDC and MDCRenderer support custom components. Components receive props from the markdown and children as React children.
Creating a Custom Alert
// 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>
)
}
Usage in markdown:
::alert{type="warning"}
This is a warning message!
::
Component with Complex Props
// components/DataTable.tsx
interface DataTableProps {
columns?: string[] // From {:columns='["Name","Age"]'}
sortable?: boolean // From {sortable}
children: React.ReactNode
}
export default function DataTable({ columns = [], sortable, children }: DataTableProps) {
return (
<table>
{columns.length > 0 && (
<thead>
<tr>
{columns.map((col, i) => (
<th key={i}>{col}</th>
))}
</tr>
</thead>
)}
<tbody>{children}</tbody>
</table>
)
}
Usage in markdown:
::data-table{:columns='["Name", "Age"]' sortable}
Table content here
::
Overriding HTML Elements
Override default element rendering:
// components/Heading.tsx
interface HeadingProps {
__node?: any
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>
)
}
const components = {
h1: Heading,
h2: Heading,
h3: Heading,
}
<MDC markdown={content} components={components} />
Props Conversion
React renderer automatically converts attributes:
| Markdown | React |
|---|---|
{class="foo"} | className="foo" |
{tabindex="0"} | tabIndex={0} |
{style="color: red"} | style={{ color: 'red' }} |
{:count="5"} | count={5} (number) |
{:data='{"key":"val"}'} | data={{ key: "val" }} (object) |
ShikiCodeBlock
Syntax-highlighted code block component using Shiki:
import { ShikiCodeBlock } from 'mdc-syntax/react'
export default function App() {
return (
<ShikiCodeBlock
language="typescript"
theme={{ light: 'github-light', dark: 'github-dark' }}
>
{`const greeting = "Hello, World!"`}
</ShikiCodeBlock>
)
}
ProsePre & ProsePreShiki
Enhanced code block components with copy functionality and styled headers. These components are part of the internal prose components system (src/react/components/prose/) and are used automatically when rendering code blocks.
Component Variants
ProsePre: Code block without syntax highlighting but with copy functionality ProsePreShiki: Code block with Shiki syntax highlighting, copy functionality, and styled header
Features
- Copy to Clipboard: Click the copy button to copy code content
- Language Label: Displays language or filename in the header
- Hover Effects: Copy button appears on hover with smooth transitions
- Loading States: ProsePreShiki shows loading state while highlighter initializes
- Fallback: Graceful fallback to plain code if highlighting fails
- Singleton Highlighter: Shared highlighter instance for performance
These components are available in the source code at packages/mdc-syntax/src/react/components/prose/ and are used internally by the prose rendering system.
TypeScript Support
import type { MinimarkTree } from 'mdc-syntax'
import { MDC, MDCRenderer } from 'mdc-syntax/react'
interface Props {
content: string
}
export default function Content({ content }: Props) {
return <MDC markdown={content} className="prose" />
}
// Custom component types
interface MDCComponentProps {
__node?: any
children?: React.ReactNode
}
interface AlertProps extends MDCComponentProps {
type?: 'info' | 'warning' | 'error' | 'success'
}
Performance Tips
- Use
MDCfor simple cases - It handles parsing internally and re-parses efficiently on content changes. - Use
MDCRendererwith caching - Pre-parse content and memoize the AST:
const mdcAst = useMemo(() => parse(content).body, [content])
- Memoize component mappings:
// Good - defined outside component
const components = { alert: Alert, card: Card }
// Or use useMemo
const components = useMemo(() => ({ alert: Alert }), [])
- Use React.memo for custom components:
export default React.memo(function Alert({ type, children }) {
return <div className={`alert-${type}`}>{children}</div>
})
See Also
- Vue Renderer - Vue integration
- Parse API - Parsing options and streaming
- MDC Syntax - Markdown component syntax