Markdown Renderer
User-safe markdown with Tailwind Typography prose, Shiki-highlighted fenced code, and Mermaid diagrams with preview-first rendering.
Private Registry
This component is distributed only from registry.private.json (Bearer token). It is not in the public registry.json. Configure @f-ui-private as in the data table docs, then install with @f-ui-private/markdown-renderer (pulls code-block and mermaid-renderer as dependencies).
The markdown-renderer registry item provides MarkdownRenderer: a client component built on react-markdown, remark-gfm, Shiki (singleton getSingletonHighlighter plus codeToHtml on fenced <code> — not the rehype plugin, which can crash on sparse HAST children), and Mermaid for fenced mermaid blocks. Defaults assume untrusted input: no raw HTML, restricted link schemes, and images blocked unless you pass a host allowlist.
Prerequisites
Tailwind Typography (`prose`)
This site’s CSS already loads Fumadocs’ preset, which includes @fumadocs/tailwind/typography (prose / not-prose). Do not also add @plugin "@tailwindcss/typography" in the same global stylesheet — two typography plugins fight over the same utilities and doc typography breaks.
If you install markdown-renderer in an app without Fumadocs, add @tailwindcss/typography and @plugin "@tailwindcss/typography"; (Tailwind v4) so prose on MarkdownRenderer works.
Installation
1. Configure components.json
Add @f-ui-private (same as Data Table).
2. Install
REGISTRY_TOKEN=xxx pnpm dlx shadcn@latest add @f-ui-private/markdown-rendererREGISTRY_TOKEN=xxx npx shadcn@latest add @f-ui-private/markdown-rendererREGISTRY_TOKEN=xxx yarn dlx shadcn@latest add @f-ui-private/markdown-rendererREGISTRY_TOKEN=xxx bun x shadcn@latest add @f-ui-private/markdown-rendererWith a namespace for the public registry only: pnpm dlx shadcn@latest add @f-ui/date — markdown-renderer itself is @f-ui-private/markdown-renderer, not on the public index.
This item declares registryDependencies: code-block, mermaid-renderer — the CLI installs those private items if missing.
Contributors
Local build: pnpm registry:build:private — install from .registry-private/r/markdown-renderer.json.
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" />;
}Demo
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
<script>alert(1)</script>"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>
);
}Security defaults
- No
rehype-raw: arbitrary HTML in the string is not rendered as DOM HTML. - Links:
http,https,mailto, and same-page#fragmentonly. Externalhttp(s)links open in a new tab withrel="noopener noreferrer nofollow ugc"unlesstrustedLinksis set. - Images: remote images are off until you pass
allowedImageHosts(hostname allowlist). - Mermaid: diagram source is executed by the Mermaid runtime on the client only; invalid diagrams surface an error and switch to the code view.
API
| Prop | Type | Default | Description |
|---|---|---|---|
source | string | (required) | Markdown string. |
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. |
allowedImageHosts | readonly string[] | — | If empty or omitted, <img> is never rendered (alt text only). |
defaultMermaidView | "code" | "preview" | "preview" | Initial view for fenced Mermaid blocks. |
mermaidTheme | string | — | Overrides auto Mermaid theme (light→default, dark→dark). |
remarkPlugins | PluggableList | — | Appended after remark-gfm. |
rehypePlugins | PluggableList | — | Appended after built-in rehype plugins. |
components | Components | — | Shallow-merged over secure defaults (override at your own risk). |
mermaidConfig | MermaidConfig | — | Passed to mermaid.initialize. |
onMermaidError | (err: unknown) => void | — | Optional logging hook. |
baseOrigin | string | — | Used to decide if a link is “external” for target/rel. |
Mermaid and Code Fences
- Fenced
mermaidblocks use MermaidRenderer (preview-first; see demo). - Other fenced languages are highlighted with Shiki when the language is bundled in the renderer’s language set; unknown languages fall back per Shiki options.
Mermaid Renderer
Preview-first Mermaid diagrams with zoom, pan, download, fullscreen, and a code toggle backed by CodeBlock.
Data Table
Schema-driven TanStack Table with toolbar, filters, URL state, inline editing, batch actions, and JSON Logic — install from the private registry with Bearer token auth.