streamdown
npx skills add https://github.com/omerakben/omer-akben --skill streamdown
Agent 安装分布
Skill 文档
Streamdown v2.x
Drop-in replacement for react-markdown optimized for AI-powered streaming. Handles incomplete Markdown gracefully during token-by-token generation.
Installation
pnpm add streamdown @streamdown/code @streamdown/mermaid @streamdown/math
# Optional: @streamdown/cjk for CJK language support
Update globals.css for Tailwind:
@source "../node_modules/streamdown/dist/*.js";
Basic Usage
import { Streamdown } from "streamdown";
import { code } from "@streamdown/code";
import { mermaid } from "@streamdown/mermaid";
import { math } from "@streamdown/math";
<Streamdown
plugins={{ code, mermaid, math }}
isAnimating={isStreaming}
>
{markdownContent}
</Streamdown>
Props Reference
| Prop | Type | Default | Description |
|---|---|---|---|
children |
string |
– | Markdown content to render |
isAnimating |
boolean |
false |
Enable streaming mode (incomplete markdown handling) |
plugins |
PluginConfig |
{} |
Code, mermaid, math, cjk plugins |
components |
Components |
{} |
Custom React components for elements |
controls |
ControlsConfig |
true |
Show/hide interactive controls |
mermaid |
MermaidOptions |
– | Mermaid diagram configuration |
caret |
"block" | "circle" |
– | Streaming cursor style |
className |
string |
– | Container CSS class |
shikiTheme |
[BundledTheme, BundledTheme] |
["github-light", "github-dark"] |
Shiki themes [light, dark] |
linkSafety |
LinkSafetyConfig |
– | External link confirmation modal |
remend |
RemendOptions |
– | Incomplete markdown parsing options |
parseIncompleteMarkdown |
boolean |
true |
Use remend for streaming |
Plugins
Code Plugin (@streamdown/code)
Shiki-powered syntax highlighting with copy/download buttons.
import { code, createCodePlugin } from "@streamdown/code";
// Default
plugins={{ code }}
// Custom themes
const customCode = createCodePlugin({
themes: ["one-dark-pro", "one-light"]
});
plugins={{ code: customCode }}
Math Plugin (@streamdown/math)
KaTeX rendering for LaTeX equations.
import { math, createMathPlugin } from "@streamdown/math";
import "katex/dist/katex.min.css"; // Required!
// Default
plugins={{ math }}
// Enable single $ for inline math
const customMath = createMathPlugin({
singleDollarTextMath: true,
errorColor: "#ff0000"
});
plugins={{ math: customMath }}
Syntax:
- Inline:
$E = mc^2$or\\(E = mc^2\\) - Block:
$$\sum_{i=1}^n x_i$$or\\[...\\]
Mermaid Plugin (@streamdown/mermaid)
Interactive diagram rendering.
import { mermaid, createMermaidPlugin } from "@streamdown/mermaid";
plugins={{ mermaid }}
// With config prop
<Streamdown
plugins={{ mermaid }}
mermaid={{
config: {
theme: "base",
themeVariables: {
primaryColor: "#73000a",
primaryTextColor: "#ffffff",
}
},
errorComponent: MyErrorComponent
}}
>
CJK Plugin (@streamdown/cjk)
WARNING: This project disabled CJK due to English text corruption. Only use if CJK language support is required.
import { cjk } from "@streamdown/cjk";
plugins={{ cjk }}
Custom Components
Override default HTML element rendering:
const components: Components = {
code({ className, children, ...props }) {
const language = className?.match(/language-(\w+)/)?.[1];
if (language) {
return <SyntaxHighlighter language={language}>{children}</SyntaxHighlighter>;
}
return <code className="inline-code" {...props}>{children}</code>;
},
pre({ children }) {
return <>{children}</>; // Let code handle wrapping
},
a({ href, children, ...props }) {
const isExternal = href?.startsWith("http");
return (
<a
href={href}
target={isExternal ? "_blank" : undefined}
rel={isExternal ? "noopener noreferrer" : undefined}
{...props}
>
{children}
</a>
);
},
table({ children }) {
return (
<div className="overflow-x-auto">
<table className="min-w-full">{children}</table>
</div>
);
}
};
<Streamdown components={components}>...</Streamdown>
Controls Configuration
// Enable all (default)
controls={true}
// Disable all
controls={false}
// Selective
controls={{
table: true, // Table copy/download
code: true, // Code copy/download
mermaid: {
download: true,
copy: true,
fullscreen: true,
panZoom: true
}
}}
Link Safety
Confirmation modal for external links:
<Streamdown
linkSafety={{
enabled: true,
onLinkCheck: async (url) => {
// Return true to auto-allow, false to show modal
return url.startsWith("https://trusted-domain.com");
},
renderModal: ({ url, isOpen, onClose, onConfirm }) => (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent>
<p>Open external link: {url}?</p>
<Button onClick={onConfirm}>Continue</Button>
</DialogContent>
</Dialog>
)
}}
>
Streaming Best Practices
Error Boundary Pattern
Wrap Streamdown in an error boundary for graceful fallback:
class StreamdownErrorBoundary extends Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return <pre className="whitespace-pre-wrap">{this.props.fallback}</pre>;
}
return this.props.children;
}
}
<StreamdownErrorBoundary fallback={rawContent}>
<Streamdown isAnimating={isStreaming}>{content}</Streamdown>
</StreamdownErrorBoundary>
Post-Stream Cleanup
Clean incomplete markers when streaming ends:
const processedContent = useMemo(() => {
if (isStreaming || !content?.trim()) return content;
let cleaned = content;
// Remove trailing incomplete bold/italic
cleaned = cleaned.replace(/\s*\*{1,2}\s*$/, "");
// Balance orphaned **
const boldMatches = cleaned.match(/\*\*/g);
if (boldMatches && boldMatches.length % 2 !== 0) {
cleaned = cleaned.replace(/\*\*(\s*)$/, "$1");
}
return cleaned;
}, [content, isStreaming]);
Lazy Loading
Reduce initial bundle size:
const StreamdownContent = lazy(() =>
import("./streamdown-content").then((mod) => ({
default: mod.StreamdownContent,
}))
);
<Suspense fallback={<PlainTextFallback content={content} />}>
<StreamdownContent content={content} isStreaming={isStreaming} />
</Suspense>
Common Issues
CJK Plugin Corrupts English Text
Symptom: Random Chinese characters like “è½” appear in English text.
Solution: Remove @streamdown/cjk plugin unless CJK support is required.
Mermaid Diagrams Crash
Symptom: Invalid mermaid syntax causes render error. Solution: Use error boundary with fallback + custom errorComponent prop.
Math Not Rendering
Symptom: Raw LaTeX syntax displayed.
Solution: Ensure katex/dist/katex.min.css is imported.
Code Blocks Unstyled
Symptom: Code blocks have no syntax highlighting.
Solution: Add @source directive in globals.css and use @streamdown/code plugin.
Streaming Markers Persist
Symptom: Trailing ** or * visible after streaming ends.
Solution: Implement post-stream content cleanup (see pattern above).
Malformed Nested Bold
Symptom: Raw ** appears before keywords in bullet points, e.g., **The P-O-L-C **Framework**:.
Cause: AI model tries to emphasize a keyword within already-bold text, creating invalid nested markdown.
Solution: Add cleanup regex in post-stream processing:
// Fix malformed nested bold: "**prefix **keyword**:" â "**prefix keyword**:"
cleaned = cleaned.replace(/\*\*([^*]+)\s\*\*(\w+)\*\*(:?)/g, "**$1 $2**$3");
TypeScript Types
import type {
StreamdownProps,
Components,
PluginConfig,
ControlsConfig,
MermaidOptions,
LinkSafetyConfig,
CodeHighlighterPlugin,
DiagramPlugin,
MathPlugin,
CjkPlugin,
} from "streamdown";
import type { BundledLanguage, BundledTheme } from "shiki";
Integration with AI SDK
import { useChat } from "@ai-sdk/react";
export function Chat() {
const { messages, status } = useChat();
const isStreaming = status === "streaming";
return (
<div>
{messages.map((message) => (
<div key={message.id}>
{message.parts.map((part, i) =>
part.type === "text" ? (
<Streamdown
key={i}
plugins={{ code, mermaid, math }}
isAnimating={isStreaming && i === message.parts.length - 1}
>
{part.text}
</Streamdown>
) : null
)}
</div>
))}
</div>
);
}
For project-specific implementation patterns, see components/ui/streamdown-content.tsx.