1import { cjk } from "@streamdown/cjk";2import { code } from "@streamdown/code";3import { math } from "@streamdown/math";4import { mermaid } from "@streamdown/mermaid";5import { useInView } from "motion/react";6import { useEffect, useRef, useState } from "react";7
8import { Markdown } from "@/components/ui/pxl/markdown";9
10const markdown = `# Streamdown11
12Streamdown is a **streaming-optimized** Markdown renderer for React. It was designed for AI chat interfaces where content arrives token-by-token, but it works just as well for static content.13
14Most Markdown renderers re-parse the entire document on every update. Streamdown takes a different approach — it splits content into discrete blocks and only re-renders the block that changed. This means your UI stays fast, even when the response is hundreds of lines long.15
16## Getting started17
18Install the package from npm, then drop it into your component tree. It accepts a \`children\` string and handles the rest.19
20\`\`\`tsx21import { Streamdown } from "streamdown";22
23const Chat = ({ content }: { content: string }) => (24 <Streamdown animated caret="block">25 {content}26 </Streamdown>27);28\`\`\`29
30The \`animated\` prop enables a smooth fade-in on new blocks, and \`caret\` renders a blinking cursor at the end of the stream — just like the one you're watching right now.31
32## Plugin ecosystem33
34Streamdown ships with optional plugins for common use cases. Each one is a separate package, so you only bundle what you need.35
36| Plugin | Package | Purpose |37| --- | --- | --- |38| Syntax highlighting | \`@streamdown/code\` | Shiki-powered code blocks |39| Diagrams | \`@streamdown/mermaid\` | Mermaid diagram rendering |40| Math | \`@streamdown/math\` | KaTeX math expressions |41| CJK | \`@streamdown/cjk\` | CJK line-breaking rules |42
43For example, the quadratic formula renders beautifully: $$x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}$$44
45## Why Streamdown?46
47There are plenty of Markdown renderers out there, but most of them weren't built for streaming. Here's what makes Streamdown different:48
49- [x] Block-level diffing for *incremental* re-renders50- [x] First-class support for ~~incomplete~~ partial Markdown51- [x] Configurable caret styles and animations52- [ ] World domination53
54> Streamdown is open-source and MIT licensed. Contributions are welcome.55`;56
57const speed = 100;58const bottomThreshold = 8;59
60const tokens = markdown.split(" ").map((token) => `${token} `);61
62export default function MarkdownDemo() {63 const [content, setContent] = useState("");64 const [isAnimating, setIsAnimating] = useState(false);65 const ref = useRef<HTMLDivElement>(null);66 const intervalRef = useRef<NodeJS.Timeout | null>(null);67 const isInView = useInView(ref, { once: true, margin: "-100px" });68 const stickToBottomRef = useRef(true);69
70 const isProgrammaticScroll = useRef(false);71
72 function handleScroll() {73 if (isProgrammaticScroll.current) {74 isProgrammaticScroll.current = false;75 return;76 }77 const el = ref.current;78 if (!el) return;79 const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;80 stickToBottomRef.current = distanceFromBottom <= bottomThreshold;81 }82
83 // biome-ignore lint/correctness/useExhaustiveDependencies: re-run on content change84 useEffect(() => {85 const el = ref.current;86 if (!el || !stickToBottomRef.current) return;87 isProgrammaticScroll.current = true;88 el.scrollTop = el.scrollHeight;89 }, [content]);90
91 useEffect(() => {92 if (!isInView) {93 return;94 }95
96 if (intervalRef.current) {97 clearInterval(intervalRef.current);98 }99
100 setContent("");101 setIsAnimating(true);102 let currentContent = "";103 let index = 0;104
105 intervalRef.current = setInterval(() => {106 if (index < tokens.length) {107 currentContent += tokens[index];108 setContent(currentContent);109 index += 1;110 } else {111 if (intervalRef.current) {112 clearInterval(intervalRef.current);113 }114 setIsAnimating(false);115 }116 }, speed);117
118 return () => {119 if (intervalRef.current) {120 clearInterval(intervalRef.current);121 }122 };123 }, [isInView]);124
125 return (126 <div ref={ref}127 onScroll={handleScroll}128 className="max-h-72 overflow-auto">129 <Markdown130 mode="streaming"131 animated132 caret="block"133 isAnimating={isAnimating}134 plugins={{ code, mermaid, math, cjk }}135 >136 {content}137 </Markdown>138 </div>139 );140}The markdown component is built using the Streamdown library.
Installation
Section titled “Installation”pnpm dlx shadcn@latest add pxl-ui/registry/markdown
Install the following dependencies:
pnpm add streamdown
Copy and paste the following code into your project.
1import { type ComponentProps, useCallback, useState } from "react";2import {3 type LinkSafetyModalProps,4 Streamdown,5 type StreamdownTranslations,6} from "streamdown";7
8import { Button } from "@/components/ui/pxl/button";9import {10 Dialog,11 DialogContent,12 DialogDescription,13 DialogFooter,14 DialogHeader,15 DialogTitle,16} from "@/components/ui/pxl/dialog";17import { cn } from "@/lib/utils";18
19const DEFAULT_MARKDOWN_TRANSLATIONS: StreamdownTranslations = {20 // Code block21 copyCode: "Copy Code",22 downloadFile: "Download file",23 // Mermaid24 downloadDiagram: "Download diagram",25 downloadDiagramAsSvg: "Download diagram as SVG",26 downloadDiagramAsPng: "Download diagram as PNG",27 downloadDiagramAsMmd: "Download diagram as MMD",28 viewFullscreen: "View fullscreen",29 exitFullscreen: "Exit fullscreen",30 mermaidFormatSvg: "SVG",31 mermaidFormatPng: "PNG",32 mermaidFormatMmd: "MMD",33 // Table34 copyTable: "Copy table",35 copyTableAsMarkdown: "Copy table as Markdown",36 copyTableAsCsv: "Copy table as CSV",37 copyTableAsTsv: "Copy table as TSV",38 downloadTable: "Download table",39 downloadTableAsCsv: "Download table as CSV",40 downloadTableAsMarkdown: "Download table as Markdown",41 tableFormatMarkdown: "Markdown",42 tableFormatCsv: "CSV",43 tableFormatTsv: "TSV",44 // Image45 imageNotAvailable: "Image not available",46 downloadImage: "Download image",47 // Link modal48 openExternalLink: "Open external link?",49 externalLinkWarning: "You're about to visit an external website.",50 close: "Close",51 copyLink: "Copy link",52 copied: "Copied",53 openLink: "Open link",54};55
56function LinkSafetyModal({57 url,58 isOpen,59 onClose,60 onConfirm,61 t = DEFAULT_MARKDOWN_TRANSLATIONS,62}: LinkSafetyModalProps & {63 t?: Partial<StreamdownTranslations>;64}) {65 const [copied, setCopied] = useState(false);66
67 const handleCopy = useCallback(async () => {68 try {69 await navigator.clipboard.writeText(url);70 setCopied(true);71 setTimeout(() => setCopied(false), 2000);72 } catch {73 // Clipboard API not available74 }75 }, [url]);76
77 const handleConfirm = useCallback(() => {78 onConfirm();79 onClose();80 }, [onConfirm, onClose]);81
82 if (!isOpen) return null;83 return (84 <Dialog open data-streamdown="link-safety-modal" onOpenChange={onClose}>85 <DialogContent>86 <DialogHeader>87 <DialogTitle>88 <svg89 xmlns="http://www.w3.org/2000/svg"90 fill="currentColor"91 viewBox="0 0 24 24"92 >93 <path d="M17 21H5v-2h12v2ZM5 19H3V7h2v12Zm14 0h-2v-6h2v6Zm-8-4H9v-2h2v2Zm2-2h-2v-2h2v2Zm2-2h-2V9h2v2Zm6 0h-2V7h-2V5h-4V3h8v8Zm-4-2h-2V7h2v2Zm-6-2H5V5h6v2Z" />94 </svg>95 {t.openExternalLink}96 </DialogTitle>97 <DialogDescription className="flex flex-col gap-4">98 <p>{t.externalLinkWarning}</p>99 <div100 className={cn(101 "break-all pixel-rounded pixel-size-md bg-muted px-3 py-2 font-mono text-sm",102 url.length > 100 && "max-h-32 overflow-y-auto",103 )}104 >105 {url}106 </div>107 </DialogDescription>108 </DialogHeader>109 <DialogFooter className="flex [&_button]:flex-1">110 <Button variant="outline" onClick={handleCopy}>111 {copied ? (112 <>113 <svg114 xmlns="http://www.w3.org/2000/svg"115 fill="currentColor"116 viewBox="0 0 24 24"117 >118 <path d="M10 18H8v-2h2v2Zm-2-2H6v-2h2v2Zm4-2v2h-2v-2h2Zm-6 0H4v-2h2v2Zm8 0h-2v-2h2v2Zm2-2h-2v-2h2v2Zm2-2h-2V8h2v2Zm2-2h-2V6h2v2Z" />119 </svg>120 <span>{t.copied}</span>121 </>122 ) : (123 <>124 <svg125 xmlns="http://www.w3.org/2000/svg"126 fill="currentColor"127 viewBox="0 0 24 24"128 >129 <path d="M18 22h-8v-2h8v2Zm-8-2H8v-2H6v-2h2V8h2v12Zm10 0h-2V8h2v12ZM6 16H4V4h2v12ZM16 6h2v2h-8V6h4V4h2v2Zm-2-2H6V2h8v2Z"></path>130 </svg>131 <span>{t.copyLink}</span>132 </>133 )}134 </Button>135 <Button variant="primary" onClick={handleConfirm}>136 <svg137 xmlns="http://www.w3.org/2000/svg"138 fill="currentColor"139 viewBox="0 0 24 24"140 >141 <path d="M17 21H5v-2h12v2ZM5 19H3V7h2v12Zm14 0h-2v-6h2v6Zm-8-4H9v-2h2v2Zm2-2h-2v-2h2v2Zm2-2h-2V9h2v2Zm6 0h-2V7h-2V5h-4V3h8v8Zm-4-2h-2V7h2v2Zm-6-2H5V5h6v2Z" />142 </svg>143 {t.openLink}144 </Button>145 </DialogFooter>146 </DialogContent>147 </Dialog>148 );149}150
151function Markdown({152 className,153 components,154 linkSafety,155 translations = DEFAULT_MARKDOWN_TRANSLATIONS,156 ...props157}: ComponentProps<typeof Streamdown>) {158 return (159 <Streamdown160 mode="static"161 linkSafety={{162 enabled: true,163 renderModal(modalProps) {164 return <LinkSafetyModal {...modalProps} t={translations} />;165 },166 ...linkSafety,167 }}168 className={cn(169 "typeset size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0",170 className,171 )}172 components={{173 ...components,174 }}175 {...props}176 />177 );178}179
180export { DEFAULT_MARKDOWN_TRANSLATIONS, Markdown };Update the import paths to match your project setup.
import { Markdown } from "@/components/ui/pxl/markdown";<Markdown># Your markdown content</Markdown>