
Streamdown
Wire Vercel Streamdown into a React chat UI so streamed assistant markdown animates safely with sanitization and optional code highlighting.
Overview
Streamdown is an agent skill for the Build phase that shows how to render streaming AI markdown in React chat UIs using Vercel Streamdown and AI SDK patterns.
Install
npx skills add https://github.com/vercel/streamdown --skill streamdownWhat is this skill?
- Drop-in Streamdown wrapper around assistant message content with isAnimating tied to useChat loading
- Strict rehype pipeline: raw, sanitize, and harden with allowed https/mailto protocols and link prefixes
- Optional @streamdown/code plugin for fenced code blocks in AI replies
- Client component pattern matching Next.js App Router chat pages
- Targets readable incremental markdown while the model is still streaming
Adoption & trust: 1.8k installs on skills.sh; 5.3k GitHub stars; 1/3 security scanners passed (skills.sh audits).
What problem does it solve?
Your chat UI shows broken or unsafe markdown while tokens stream, and plain text makes assistant replies hard to read.
Who is it for?
Solo builders shipping Next.js or React agent chat surfaces who want Vercel-aligned streaming markdown components.
Skip if: Non-React stacks, backend-only agents with no embedded UI, or teams that do not need incremental markdown rendering.
When should I use this skill?
Building or refining a React chat interface that must display streaming assistant markdown with Streamdown.
What do I get? / Deliverables
You implement animated, sanitized Streamdown message bubbles integrated with useChat on a standard chat page layout.
- Chat page component with Streamdown-rendered assistant messages
- Rehype security configuration for AI content
Recommended Skills
Journey fit
How it compares
Frontend component integration skill—not an MCP server or a prompt library for model behavior.
Common Questions / FAQ
Who is streamdown for?
Streamdown is for developers building React or Next.js chat products who consume Vercel’s Streamdown package with @ai-sdk/react.
When should I use streamdown?
Use it in Build/frontend while implementing assistant message UI, hardening markdown sanitization, or adding code-block rendering to a streaming chat page.
Is streamdown safe to install?
The examples promote sanitize and harden plugins for AI-generated HTML; review the Security Audits panel on this page and keep rehype config strict in production.
SKILL.md
READMESKILL.md - Streamdown
"use client"; import { useChat } from "@ai-sdk/react"; import { Streamdown } from "streamdown"; export default function ChatPage() { const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat(); return ( <div className="flex h-screen flex-col"> <div className="flex-1 space-y-4 overflow-y-auto p-4"> {messages.map((message) => ( <div className={message.role === "user" ? "text-right" : "text-left"} key={message.id} > <div className="inline-block max-w-2xl"> <Streamdown isAnimating={isLoading && message.role === "assistant"} > {message.content} </Streamdown> </div> </div> ))} </div> <form className="border-t p-4" onSubmit={handleSubmit}> <input className="w-full rounded-lg border px-4 py-2" disabled={isLoading} onChange={handleInputChange} placeholder="Ask me anything..." value={input} /> </form> </div> ); } "use client"; import { code } from "@streamdown/code"; import { defaultRehypePlugins, Streamdown } from "streamdown"; // Strict security config for AI-generated content const rehypePlugins = [ defaultRehypePlugins.raw, defaultRehypePlugins.sanitize, [ defaultRehypePlugins.harden[0], { allowedProtocols: ["https", "mailto"], allowedLinkPrefixes: [ "https://your-domain.com", "https://docs.your-domain.com", ], allowedImagePrefixes: ["https://cdn.your-domain.com"], allowDataImages: false, }, ], ]; export default function SecureChat({ content }: { content: string }) { return ( <Streamdown linkSafety={{ enabled: true, onLinkCheck: (url) => { const trusted = ["your-domain.com"]; const hostname = new URL(url).hostname; return trusted.some((d) => hostname.endsWith(d)); }, }} plugins={{ code }} rehypePlugins={rehypePlugins} > {content} </Streamdown> ); } "use client"; import { useChat } from "@ai-sdk/react"; import { code } from "@streamdown/code"; import { math } from "@streamdown/math"; import { mermaid } from "@streamdown/mermaid"; import { Streamdown } from "streamdown"; import "katex/dist/katex.min.css"; export default function FullFeaturedChat() { const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat(); return ( <div className="flex h-screen flex-col"> <div className="flex-1 space-y-4 overflow-y-auto p-4"> {messages.map((message, index) => ( <div key={message.id}> <Streamdown caret="block" controls={{ code: true, table: true, mermaid: { download: true, copy: true, fullscreen: true, panZoom: true, }, }} isAnimating={ isLoading && index === messages.length - 1 && message.role === "assistant" } linkSafety={{ enabled: true, onLinkCheck: (url) => { const trusted = ["github.com", "npmjs.com"]; const hostname = new URL(url).hostname; return trusted.some((d) => hostname.endsWith(d)); }, }} plugins={{ code, mermaid, math }} > {message.content} </Streamdown> </div> ))} </div> <form className="border-t p-4" onSubmit={handleSubmit}> <input className="w-full rounded-lg border px-4 py-2" disabled={isLoading} onChange={handleInputChange} placeholder="Ask me anything..." value={input}