f-ui
Components

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-renderer
REGISTRY_TOKEN=xxx npx shadcn@latest add @f-ui-private/markdown-renderer
REGISTRY_TOKEN=xxx yarn dlx shadcn@latest add @f-ui-private/markdown-renderer
REGISTRY_TOKEN=xxx bun x shadcn@latest add @f-ui-private/markdown-renderer

With a namespace for the public registry only: pnpm dlx shadcn@latest add @f-ui/datemarkdown-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

ts
Loading syntax highlight

Mermaid

Loading MermaidLoading diagram…

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 #fragment only. External http(s) links open in a new tab with rel="noopener noreferrer nofollow ugc" unless trustedLinks is 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

PropTypeDefaultDescription
sourcestring(required)Markdown string.
classNamestringMerged onto the outer <article> (after prose classes).
proseClassNamestringExtra classes merged with prose dark:prose-invert max-w-none.
trustedLinksbooleanfalseWhen true, external links omit nofollow ugc.
allowedImageHostsreadonly string[]If empty or omitted, <img> is never rendered (alt text only).
defaultMermaidView"code" | "preview""preview"Initial view for fenced Mermaid blocks.
mermaidThemestringOverrides auto Mermaid theme (light→default, dark→dark).
remarkPluginsPluggableListAppended after remark-gfm.
rehypePluginsPluggableListAppended after built-in rehype plugins.
componentsComponentsShallow-merged over secure defaults (override at your own risk).
mermaidConfigMermaidConfigPassed to mermaid.initialize.
onMermaidError(err: unknown) => voidOptional logging hook.
baseOriginstringUsed to decide if a link is “external” for target/rel.

Mermaid and Code Fences

  • Fenced mermaid blocks 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.

On this page