Markdown Renderer
Render user-generated markdown safely with GFM prose, KaTeX math, sanitized HTML tables, Shiki code blocks, and interactive Mermaid diagrams.
Plus Registry
This component ships from registry.plus.json, not the public registry.json. Set up @f-ui-plus on the Installation page, then install @f-ui-plus/markdown-renderer (pulls code-block and mermaid-renderer).
Markdown Renderer turns a markdown string into styled article content for comments, knowledge bases, AI chat, and other untrusted user input. It ships secure defaults (sanitized HTML when enabled, restricted links, images off by default), GFM via remark-gfm, KaTeX math, Shiki-highlighted fenced code through Code Block, and preview-first Mermaid diagrams through Mermaid Renderer.
When To Use
- Display user-authored or AI-generated markdown where XSS and open redirects are a concern.
- You need GFM (tables, task lists, strikethrough) plus polished code fences without wiring react-markdown yourself.
- Fenced
mermaidblocks should render as interactive diagrams (zoom, export, fullscreen), not plain highlighted text. - You want a single component for article prose, or a headless hook to reuse the same plugin and element map in a custom layout.
- Need phrasing markdown inside a label, chip, table cell, or button — use
InlineMarkdown(root<span>, no article/streaming). UseMarkdownRendererfor document prose and streaming replies. - Large static or host-segmented preview — use Markdown Document (virtualized segments; inherits this component’s KaTeX / HTML tables / image policy; not for streaming).
- Knowledge-base / document-library List↔Detail peek of processed markdown — example: /showcases/knowledge-base-documents (Sheet Preview tab).
Features
| Area | Behavior |
|---|---|
| Prose | prose / dark:prose-invert on the root <article> (standard Tailwind Typography) |
| GFM | Tables (local horizontal scroll when wide), task lists, strikethrough, autolinks (via remark-gfm) |
| Math | $…$ / $$…$$, \(...\), and \[…\] via KaTeX by default; fenced and inline code stay literal. Set math={false} for currency-heavy text |
| HTML tables | Author <table> (incl. rowspan / colspan) renders through table chrome when allowHtml is on (default); $…$ inside cell text also becomes KaTeX |
| Block rhythm | Custom fences / tables / Mermaid use my-6 so gaps match prose; article resets first/last child margins |
| Code fences | Code Block with chrome and line numbers; mainstream + on-demand Shiki grammars; plain-first while highlighting |
| Mermaid fences | Mermaid Renderer (preview-first; errors switch to code view) |
| Links | http, https, mailto, and #fragment only; blocked schemes render as plain text |
| External links | Open in a new tab; rel includes nofollow ugc unless trustedLinks |
Images (MarkdownRenderer) | Off by default; opt in with allowImages, allowedImageHosts, allowedImagePrefixes, or allowDataImages. Default frame is imageVariant="outline" (flush bitmap). Optional imageZoom, imageObjectFit. |
| Streaming | Set isStreaming while the source is still growing: remend repairs incomplete markdown, finished blocks stay memoized, and an unclosed \[ does not swallow the rest of the message |
| Raw HTML | Parsed when allowHtml (default true), then sanitized; scripts and other dangerous tags never become DOM. Set allowHtml={false} to restore literal tag text |
| Extension | Append remarkPlugins / rehypePlugins; shallow-merge components |
Tailwind Typography
MarkdownRenderer applies standard prose / dark:prose-invert. Install @tailwindcss/typography and add @plugin "@tailwindcss/typography"; in your global CSS (Tailwind v4). No other f-ui-specific typography setup is required in a normal app layout.
Fumadocs doc sites only
If demos are wrapped in not-prose (as on this site), nested prose does not apply. This repo registers a second typography namespace — fui-md-prose — in src/styles/app.css. The component includes those classes for you; registry consumers can ignore this unless they use the same Fumadocs + not-prose demo pattern.
Installing
Configure @f-ui-plus and FUI_PLUS_REGISTRY_TOKEN as in Installation — Plus Registry.
FUI_PLUS_REGISTRY_TOKEN=xxx pnpm dlx shadcn@latest add @f-ui-plus/markdown-rendererFUI_PLUS_REGISTRY_TOKEN=xxx npx shadcn@latest add @f-ui-plus/markdown-rendererFUI_PLUS_REGISTRY_TOKEN=xxx yarn dlx shadcn@latest add @f-ui-plus/markdown-rendererFUI_PLUS_REGISTRY_TOKEN=xxx bun x shadcn@latest add @f-ui-plus/markdown-rendererWith a namespace for the public registry only: pnpm dlx shadcn@latest add @f-ui/date-picker — markdown-renderer itself is @f-ui-plus/markdown-renderer, not on the public index.
registryDependencies: code-block, mermaid-renderer, document-preview. Runtime: react-markdown, remark-gfm, remark-math-extended, rehype-katex, katex, rehype-raw, rehype-sanitize, hast-util-sanitize, shiki, mermaid, hast-util-to-string, remend, marked.
Usage
import { MarkdownRenderer } from "@/components/f-ui/markdown-renderer/markdown-renderer";
const source = ["# Hello", "", "```ts", "const x = 1;", "```"].join("\n");
export function Page() {
return <MarkdownRenderer source={source} className="max-w-3xl" />;
}Examples
Live Editor
Edit the source on the left; the right pane updates with secure rendering. Try a javascript: link or raw <script> — they are stripped or shown as text, not executed.
Markdown source
Rendered
Markdown renderer
User-generated safe markdown with a normal link and a blocked scheme.
List
- One
- Two
TypeScript
Mermaid
Raw angle brackets
"use client";
import { useState } from "react";
import { MarkdownRenderer } from "@/components/f-ui/markdown-renderer/markdown-renderer";
const DEFAULT_MARKDOWN = `# Markdown renderer
User-generated **safe** markdown with [a normal link](https://example.com) and a [blocked scheme](javascript:alert(1)).
## List
- One
- Two
## TypeScript
\`\`\`ts
const n: number = 42;
\`\`\`
## Mermaid
\`\`\`mermaid
flowchart LR
A[Start] --> B[End]
\`\`\`
## Raw angle brackets
<script>alert(1)</script>
`;
export function MarkdownRendererDemo() {
const [source, setSource] = useState(DEFAULT_MARKDOWN);
return (
<div className="grid gap-4 lg:grid-cols-2">
<div className="flex min-h-[320px] flex-col gap-2">
<p className="text-muted-foreground text-xs font-medium">
Markdown source
</p>
<textarea
value={source}
onChange={(e) => setSource(e.target.value)}
className="border-input bg-background text-foreground focus-visible:ring-ring min-h-[280px] flex-1 resize-y rounded-md border p-3 font-mono text-sm focus-visible:ring-2 focus-visible:outline-none"
spellCheck={false}
aria-label="Markdown source"
/>
</div>
<div className="flex min-h-[320px] flex-col gap-2">
<p className="text-muted-foreground text-xs font-medium">Rendered</p>
<div className="border-input bg-card min-h-[280px] flex-1 overflow-auto rounded-md border p-4">
<MarkdownRenderer source={source} />
</div>
</div>
</div>
);
}Inline Fragment
For bold, links, or inline code inside chrome that cannot host an <article>, use InlineMarkdown. Check the button label, chip, table cells, and the blocked javascript: line — phrasing stays inside the host with no article/prose margins. There is no inline mode prop on MarkdownRenderer.
"use client";
import { InlineMarkdown } from "@/components/f-ui/markdown-renderer/inline-markdown";
import { Button } from "@/components/ui/button";
/**
* Host surfaces where an `<article>` / prose shell would break layout:
* button label, chip, table cell, muted status line, blocked scheme.
*/
export function MarkdownRendererInlineFragmentDemo() {
return (
<div className="flex flex-col gap-4">
<div className="flex flex-wrap items-center gap-3">
<Button type="button" variant="outline">
<InlineMarkdown source={"Open **docs**"} />
</Button>
<span className="border-border bg-muted/40 inline-flex items-center rounded-md border px-2 py-0.5 text-xs">
<InlineMarkdown source={"Priority: `P1` — **ship today**"} />
</span>
<p className="text-muted-foreground text-sm">
Status:{" "}
<InlineMarkdown
source={"Ready — see [changelog](https://example.com)"}
trustedLinks
baseOrigin="https://app.local"
/>
</p>
</div>
<div className="border-border overflow-hidden rounded-md border">
<table className="w-full text-sm">
<thead>
<tr className="border-border bg-muted/30 border-b text-left">
<th className="px-3 py-2 font-medium">Field</th>
<th className="px-3 py-2 font-medium">Value</th>
</tr>
</thead>
<tbody>
<tr className="border-border border-b">
<td className="text-muted-foreground px-3 py-2">Owner</td>
<td className="px-3 py-2">
<InlineMarkdown
source={"Assigned to [Ada](https://example.com/ada)"}
trustedLinks
baseOrigin="https://app.local"
/>
</td>
</tr>
<tr>
<td className="text-muted-foreground px-3 py-2">Note</td>
<td className="px-3 py-2">
<InlineMarkdown source={"Use `InlineMarkdown` in cells — not an article"} />
</td>
</tr>
</tbody>
</table>
</div>
<p className="text-muted-foreground text-sm">
Blocked scheme (link text only):{" "}
<InlineMarkdown source={"[unsafe](javascript:alert(1))"} />
</p>
</div>
);
}Wide Tables
Multi-column GFM tables wrap in a local horizontal scroll container (w-max table inside overflow-x-auto). This demo uses a narrow card so the table must scroll instead of stretching the page — the same behavior in a chat measure.
Comparison across regions (scroll horizontally when the pane is narrow):
| Metric | North America | EMEA | APAC | LATAM | ANZ | Middle East | Africa | Nordics | Benelux | DACH |
|---|---|---|---|---|---|---|---|---|---|---|
| Active seats | 12,480 | 9,210 | 14,902 | 3,441 | 2,108 | 1,776 | 890 | 1,204 | 980 | 2,450 |
| Median latency (ms) | 48 | 62 | 91 | 110 | 74 | 88 | 130 | 41 | 55 | 58 |
| Error budget remaining | 99.2% | 98.7% | 97.9% | 99.0% | 99.4% | 98.1% | 97.2% | 99.6% | 99.1% | 98.8% |
| Last deploy window | 2026-07-24T18:00Z | 2026-07-24T16:30Z | 2026-07-25T02:15Z | 2026-07-24T22:00Z | 2026-07-25T01:00Z | 2026-07-24T20:45Z | 2026-07-24T19:10Z | 2026-07-24T15:00Z | 2026-07-24T14:20Z | 2026-07-24T13:55Z |
"use client";
import { MarkdownRenderer } from "@/components/f-ui/markdown-renderer/markdown-renderer";
/** Many columns + long cell text so the table exceeds a chat-like measure. */
const WIDE_TABLE = [
"Comparison across regions (scroll horizontally when the pane is narrow):",
"",
"| Metric | North America | EMEA | APAC | LATAM | ANZ | Middle East | Africa | Nordics | Benelux | DACH |",
"| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |",
"| Active seats | 12,480 | 9,210 | 14,902 | 3,441 | 2,108 | 1,776 | 890 | 1,204 | 980 | 2,450 |",
"| Median latency (ms) | 48 | 62 | 91 | 110 | 74 | 88 | 130 | 41 | 55 | 58 |",
"| Error budget remaining | 99.2% | 98.7% | 97.9% | 99.0% | 99.4% | 98.1% | 97.2% | 99.6% | 99.1% | 98.8% |",
"| Last deploy window | `2026-07-24T18:00Z` | `2026-07-24T16:30Z` | `2026-07-25T02:15Z` | `2026-07-24T22:00Z` | `2026-07-25T01:00Z` | `2026-07-24T20:45Z` | `2026-07-24T19:10Z` | `2026-07-24T15:00Z` | `2026-07-24T14:20Z` | `2026-07-24T13:55Z` |",
].join("\n");
/**
* Constrains width like a chat / docs measure so the wide GFM table must
* scroll locally instead of expanding the page.
*/
export function MarkdownRendererWideTableDemo() {
return (
<div className="border-input bg-card max-w-md overflow-hidden rounded-md border p-4">
<MarkdownRenderer source={WIDE_TABLE} />
</div>
);
}Block Rhythm
Paragraphs, tables, and fenced Code Blocks share one vertical rhythm (my-6 on custom blocks that opt out of Typography). Check the gaps above/below the table and the TypeScript fence — this demo keeps the table within the card; wide-table scroll is under Wide Tables above.
Before the table, a short paragraph should sit with even spacing above the grid.
| Region | Seats | Latency | Budget | Window | Owner | Notes |
|---|---|---|---|---|---|---|
| North America | 12,480 | 48ms | 99.2% | 18:00Z | platform | Primary |
| EMEA | 9,210 | 62ms | 98.7% | 16:30Z | platform | Failover |
| APAC | 14,902 | 91ms | 97.9% | 02:15Z | growth | Peak |
| LATAM | 3,441 | 110ms | 99.0% | 22:00Z | growth | Expanding |
After the table, the following TypeScript sample should keep the same vertical gap:
Closing sentence under the code block — spacing should match the gap above the fence.
"use client";
import { MarkdownRenderer } from "@/components/f-ui/markdown-renderer/markdown-renderer";
/**
* Mixed prose + wide table + code fence — verifies vertical rhythm (`my-6` on
* custom blocks) and local table horizontal scroll in one pass.
*/
const MIXED_SOURCE = [
"Before the table, a short paragraph should sit with even spacing above the grid.",
"",
"| Region | Seats | Latency | Budget | Window | Owner | Notes |",
"| --- | --- | --- | --- | --- | --- | --- |",
"| North America | 12,480 | 48ms | 99.2% | `18:00Z` | platform | Primary |",
"| EMEA | 9,210 | 62ms | 98.7% | `16:30Z` | platform | Failover |",
"| APAC | 14,902 | 91ms | 97.9% | `02:15Z` | growth | Peak |",
"| LATAM | 3,441 | 110ms | 99.0% | `22:00Z` | growth | Expanding |",
"",
"After the table, the following TypeScript sample should keep the same vertical gap:",
"",
"```ts",
"export function summarize(rows: Row[]) {",
" return rows.reduce((n, r) => n + r.seats, 0);",
"}",
"```",
"",
"Closing sentence under the code block — spacing should match the gap above the fence.",
].join("\n");
export function MarkdownRendererBlockRhythmDemo() {
return (
<div className="border-input bg-card max-w-lg overflow-hidden rounded-md border p-4">
<MarkdownRenderer source={MIXED_SOURCE} />
</div>
);
}Math and HTML Tables
Default math and allowHtml turn $…$ / $$…$$ into KaTeX and render author <table> markup (including rowspan) instead of escaping it. Cell text like $N$ also renders as math. InlineMarkdown shares the math pipeline only — it does not parse raw HTML.
For the long / virtualized knowledge-base path (same pipeline inside scroll segments), see Markdown Document — Math and HTML Tables.
Euler’s identity inline: .
Parenthesis delimiters: .
Attention scales as:
Bracket display:
Parsed artifact table (HTML, not GFM pipes) — cell text like $N$ becomes KaTeX via dollar-lift:
| Symbol | Meaning | Typical size |
|---|---|---|
| Sequence length | ||
| Batch size (same row-span symbol) | ||
| Model width |
For the virtualized knowledge-base path with the same pipeline, open Markdown Document → Math and HTML Tables.
"use client";
import { MarkdownRenderer } from "@/components/f-ui/markdown-renderer/markdown-renderer";
/**
* MinerU / paper-style fixture: KaTeX in prose plus an HTML `<table>` whose
* cells contain `$…$` (opaque to remark-math until the dollar-lift pass).
*/
const MATH_HTML_TABLE_SOURCE = [
"Euler’s identity inline: $e^{i\\pi}+1=0$.",
"",
"Parenthesis delimiters: \\(a^2+b^2=c^2\\).",
"",
"Attention scales as:",
"",
"$$",
"\\mathrm{Attention}(Q,K,V)=\\mathrm{softmax}\\left(\\frac{QK^{\\top}}{\\sqrt{d_k}}\\right)V",
"$$",
"",
"Bracket display:",
"",
"\\[",
"\\mathcal{L}_{\\mathrm{CE}} = -\\sum_i y_i \\log \\hat{y}_i",
"\\]",
"",
"Parsed artifact table (HTML, not GFM pipes) — cell text like `$N$` becomes KaTeX via dollar-lift:",
"",
"<table>",
"<thead>",
"<tr><th>Symbol</th><th>Meaning</th><th>Typical size</th></tr>",
"</thead>",
"<tbody>",
'<tr><td rowspan="2">$N$</td><td>Sequence length</td><td>$512$</td></tr>',
"<tr><td>Batch size (same row-span symbol)</td><td>$32$</td></tr>",
"<tr><td>$d_{model}$</td><td>Model width</td><td colspan=\"1\">$768$</td></tr>",
"</tbody>",
"</table>",
"",
"For the **virtualized** knowledge-base path with the same pipeline, open **Markdown Document → Math and HTML Tables**.",
].join("\n");
export function MarkdownRendererMathHtmlTableDemo() {
return (
<div className="border-input bg-card max-w-lg overflow-hidden rounded-md border p-4">
<MarkdownRenderer source={MATH_HTML_TABLE_SOURCE} />
</div>
);
}Allow Images
Images are deny-by-default. Set allowImages to permit any safe http(s) URL, or tighten with allowedImageHosts / allowedImagePrefixes (and allowDataImages for data:image/…). Allowed images use an outline frame by default (subtle border, bitmap flush to the edge). This demo uses a hostname allowlist — compare the allowed picsum.photos image with the blocked example.com placeholder.
Images
Remote images are off by default. Use allowImages for all safe http(s) URLs, or restrict with allowedImageHosts / allowedImagePrefixes. This demo uses a hostname allowlist — picsum.photos renders; example.com stays blocked.
Image blocked: Blocked
"use client"
import { MarkdownRenderer } from "@/components/f-ui/markdown-renderer/markdown-renderer"
const SOURCE = [
"# Images",
"",
"Remote images are off by default. Use **`allowImages`** for all safe `http(s)` URLs, or restrict with **`allowedImageHosts`** / **`allowedImagePrefixes`**. This demo uses a hostname allowlist — `picsum.photos` renders; `example.com` stays blocked.",
"",
"",
"",
"",
].join("\n")
export function MarkdownRendererAllowedImagesDemo() {
return (
<MarkdownRenderer
source={SOURCE}
allowedImageHosts={["picsum.photos"]}
/>
)
}Image Zoom
Pass imageZoom (with an image allow opt-in) to open the same Plus Document Preview image lightbox on click — shared close control (top-2 right-2), p-0 shell, and zoom / rotate / download toolbar. Dismiss with Escape or the overlay.
"use client"
import { MarkdownRenderer } from "@/components/f-ui/markdown-renderer/markdown-renderer"
export function MarkdownRendererImageZoomDemo() {
return (
<MarkdownRenderer
source={""}
allowImages
imageZoom
/>
)
}Streaming Source
Pass isStreaming while characters are still arriving. Incomplete emphasis is repaired mid-stream. This demo also opts into images so a late image line can load.
When Streaming Completes
Leaving isStreaming true after the source stops growing is fine for static display — finished blocks stay memoized. Setting it to false remounts as a single pass (Code Block, Mermaid, and images may reload). Prefer keeping isStreaming true after complete when remounts are undesirable.
Streaming…
"use client";
import { useEffect, useState } from "react";
import { MarkdownRenderer } from "@/components/f-ui/markdown-renderer/markdown-renderer";
import { Button } from "@/components/ui/button";
const FULL_SOURCE = [
"# Streaming source",
"",
"Incomplete **emphasis** closes only after more tokens arrive.",
"",
"",
"",
"Done.",
].join("\n");
const CHAR_INTERVAL_MS = 28;
export function MarkdownRendererStreamingDemo() {
const [visibleLength, setVisibleLength] = useState(0);
const [runId, setRunId] = useState(0);
useEffect(() => {
setVisibleLength(0);
const id = window.setInterval(() => {
setVisibleLength((prev) => {
if (prev >= FULL_SOURCE.length) {
window.clearInterval(id);
return prev;
}
return prev + 1;
});
}, CHAR_INTERVAL_MS);
return () => window.clearInterval(id);
}, [runId]);
const source = FULL_SOURCE.slice(0, visibleLength);
const isStreaming = visibleLength < FULL_SOURCE.length;
return (
<div className="flex flex-col gap-3">
<div className="flex items-center gap-3">
<Button
type="button"
variant="outline"
onClick={() => setRunId((n) => n + 1)}
>
Replay
</Button>
<p className="text-muted-foreground text-xs">
{isStreaming ? "Streaming…" : "Complete"}
</p>
</div>
<div className="border-input bg-card min-h-[200px] overflow-auto rounded-md border p-4">
<MarkdownRenderer source={source} isStreaming={isStreaming} allowImages />
</div>
</div>
);
}Chat hosts
When rendering a growing assistant reply inside a message list, pass
isStreaming={true} until the turn completes so remend and block
memoization stay active. See Message Scroller
for the shell and the assistant-first recipe; do not wrap long assistant
markdown in a solid Bubble by default.
Headless Usage
useMarkdownRenderer is the view-model: resolved className, secure components, and built-in remarkPlugins / rehypePlugins. Spread them onto ReactMarkdown when you own the outer layout or need multiple markdown regions in one tree.
isStreaming is honored by the MarkdownRenderer container only (remend + block split/memo). The headless hook does not preprocess or split — prepare the source yourself, or use the container.
Source length: 121 characters
Headless markdown
Wire useMarkdownRenderer to your own shell and ReactMarkdown.
"use client"
import ReactMarkdown from "react-markdown"
import { useMarkdownRenderer } from "@/components/f-ui/markdown-renderer/use-markdown-renderer"
const SOURCE = [
"# Headless markdown",
"",
"Wire **`useMarkdownRenderer`** to your own shell and **`ReactMarkdown`**.",
"",
"```ts",
"const ok = true",
"```",
].join("\n")
export function MarkdownRendererHeadlessDemo() {
const vm = useMarkdownRenderer({ source: SOURCE })
return (
<div>
<p>Source length: {SOURCE.length} characters</p>
<article>
<ReactMarkdown
remarkPlugins={vm.remarkPlugins}
rehypePlugins={vm.rehypePlugins}
components={vm.components}
>
{SOURCE}
</ReactMarkdown>
</article>
</div>
)
}The stock MarkdownRenderer container wraps this hook and adds streaming preprocessing when isStreaming is set.
Composition
MarkdownRenderer InlineMarkdown
└── useMarkdownRenderer └── createInlineMarkdownComponents
└── ReactMarkdown └── ReactMarkdown
├── remark-gfm + remark-math-extended ├── remark-gfm + remark-math-extended
├── rehype: sanitize children → mermaid ├── rehype: sanitize children
│ fences → raw → sanitize → dollar-lift │ → dollar-lift → KaTeX
│ → KaTeX │ (no rehype-raw / HTML tables)
└── full secure element map └── inline map (a, code, unwrap p)
├── Code Block / Mermaid / images
└── links (link-policy)
Markdown Document (large preview)
└── virtual window of MarkdownDocumentSegment*
└── MarkdownRenderer ← same math / allowHtml / GFM / Mermaid / imagesSecurity
| Threat | Default |
|---|---|
Raw HTML / <script> | Parsed only when allowHtml (default on), then sanitized — script, iframes, and event handlers never become DOM nodes |
HTML <img> vs markdown images | Sanitize allows img so markdown ![]() reaches MarkdownImage. That component is the policy gate: HTML <img> cannot bypass allowImages / host / prefix allowlists |
javascript: / exotic link schemes | Link text only; no <a href> |
External http(s) SEO | rel="noopener noreferrer nofollow ugc"; set trustedLinks to drop nofollow ugc |
Remote images (MarkdownRenderer) | Deny-by-default — blocked until allowImages, allowedImageHosts, allowedImagePrefixes, or allowDataImages; blocked/error states keep reserved height |
Mermaid source (MarkdownRenderer) | Parsed and rendered on the client only; invalid diagrams show an error and switch to code view |
InlineMarkdown shares default math but does not parse raw HTML. It also does not wire image or fence chrome — if block/image markdown slips through, default react-markdown <img> can load remote URLs and is not covered by the document deny-by-default policy above.
Override components only when you accept responsibility for the security trade-offs.
API Reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
source | string | (required) | Markdown string to render. |
className | string | — | Merged onto the outer <article> (after prose classes). |
proseClassName | string | — | Extra classes merged with prose dark:prose-invert max-w-none. |
trustedLinks | boolean | false | When true, external links omit nofollow ugc. |
math | boolean | true | When true, render $…$ / $$…$$ (and \(...\) / \[…\]) with KaTeX. Set false for currency-heavy text. |
allowHtml | boolean | true | When true, parse embedded HTML (e.g. <table>) then sanitize. When false, HTML tags appear as literal text. |
allowImages | boolean | false | When true, allow http: / https: images (dangerous schemes still blocked). |
allowedImageHosts | readonly string[] | — | Hostname allowlist (compatibility); matching hosts may render when other allow opts are off. |
allowedImagePrefixes | readonly string[] | — | URL prefix allowlist (e.g. https://cdn.example.com/). |
allowDataImages | boolean | false | When true, allow data:image/… sources. |
imageVariant | "outline" | "ghost" | "muted" | "outline" | Frame chrome. Default outline = subtle border; image sits flush (no inner padding). ghost = no border; muted = soft background. |
imageObjectFit | "contain" | "cover" | "contain" | How the bitmap fills the frame. Default contain shows the full image; cover fills a fixed aspect frame and may crop. |
imageZoom | boolean | false | When true, click a loaded image to open Document Preview (image lightbox). |
onImageError | (src: string, error: unknown) => void | — | Optional callback when an allowed image fails to load. |
isStreaming | boolean | false | Container-only: when true, remend incomplete markdown and memoize finished blocks while the source grows. Leaving true after complete is fine; setting false remounts as a single pass. Ignored by useMarkdownRenderer. |
defaultMermaidView | "code" | "preview" | "preview" | Initial tab for fenced Mermaid blocks. |
mermaidTheme | string | — | Overrides auto Mermaid theme (light→default, dark→dark). |
mermaidConfig | MermaidConfig | — | Passed to mermaid.initialize. |
onMermaidError | (err: unknown) => void | — | Optional logging hook for diagram failures. |
mermaidWheelZoom | "always" | "modifier" | "off" | "modifier" | Passed to embedded Mermaid Renderer as wheelZoom. Default keeps page scroll; hold Ctrl or ⌘ and scroll to zoom the diagram in place. |
baseOrigin | string | — | Origin for deciding if a link is external (target / rel). |
remarkPlugins | PluggableList | — | Appended after remark-gfm. |
rehypePlugins | PluggableList | — | Appended after built-in rehype plugins. |
components | Components | — | Shallow-merged over secure defaults. |
Hook
useMarkdownRenderer(props) returns { className, components, remarkPlugins, rehypePlugins } — the same inputs MarkdownRenderer passes to ReactMarkdown. The hook accepts shared props (including isStreaming) for typing convenience, but does not remend or split blocks; only the container does.
InlineMarkdown Props
Lightweight phrasing renderer. Same Plus install as MarkdownRenderer. Import from @/components/f-ui/markdown-renderer/inline-markdown.
| Prop | Type | Default | Description |
|---|---|---|---|
source | string | (required) | Markdown string rendered as a fragment. |
className | string | — | Merged onto the root <span>. |
trustedLinks | boolean | false | When true, external links omit nofollow ugc. |
math | boolean | true | When true, render $…$ / $$…$$ with KaTeX (same default as MarkdownRenderer). |
baseOrigin | string | — | Origin for deciding if a link is external. |
remarkPlugins | PluggableList | — | Appended after remark-gfm. |
rehypePlugins | PluggableList | — | Appended after built-in sanitize. |
components | Components | — | Shallow-merged over inline defaults (a, inline code, unwrap p). |
Not supported on InlineMarkdown: allowHtml, isStreaming, proseClassName, image props, Mermaid props. Use MarkdownRenderer for document layouts, HTML tables, and streaming.