import { streamText } from "ai"
export async function POST(request: Request) { const { messages } = await request.json()
const result = streamText({ model: "anthropic/claude-sonnet-4.5", system: "You are a helpful assistant. Answer in one short paragraph.", messages, })
return result.toUIMessageStreamResponse()}"use client";
import { DownloadIcon, FileCodeIcon, WrapTextIcon } from "lucide-react";import * as React from "react";import { useHighlightedCode } from "@/hooks/use-highlighted-code";import { Button } from "@/components/ui/button";import { CodeBlock, CodeBlockActions, CodeBlockContent, CodeBlockCopyButton, CodeBlockHeader, CodeBlockLine, CodeBlockTitle,} from "@/components/ui/code-block";
const code = `import { streamText } from "ai"
export async function POST(request: Request) { const { messages } = await request.json()
const result = streamText({ model: "anthropic/claude-sonnet-4.5", system: "You are a helpful assistant. Answer in one short paragraph.", messages, })
return result.toUIMessageStreamResponse()}`;
function download(value: string, filename: string) { const url = URL.createObjectURL(new Blob([value], { type: "text/plain" })); const link = document.createElement("a"); link.href = url; link.download = filename; link.click(); URL.revokeObjectURL(url);}
export function CodeBlockDemo() { const [wrap, setWrap] = React.useState(false); const lines = useHighlightedCode(code, "ts");
return ( <div className="w-full max-w-lg"> <CodeBlock> <CodeBlockHeader> <CodeBlockTitle> <FileCodeIcon /> app/api/chat/route.ts </CodeBlockTitle> <CodeBlockActions> <Button variant="ghost" size="icon-xs" aria-label="Wrap lines" aria-pressed={wrap} className="aria-pressed:text-foreground" onClick={() => setWrap(!wrap)} > <WrapTextIcon /> </Button> <Button variant="ghost" size="icon-xs" aria-label="Download" onClick={() => download(code, "route.ts")} > <DownloadIcon /> </Button> <CodeBlockCopyButton value={code} /> </CodeBlockActions> </CodeBlockHeader> <CodeBlockContent wrap={wrap} lineNumbers> {lines.map((line, index) => ( <CodeBlockLine key={index} variant={index >= 5 && index <= 9 ? "highlighted" : "default"} > {line} </CodeBlockLine> ))} </CodeBlockContent> </CodeBlock> </div> );}pnpm dlx shadcn@latest add @herocn/code-blockcode-block also installs useHighlightedCode for syntax highlighting.
import { CodeBlock, CodeBlockActions, CodeBlockContent, CodeBlockCopyButton, CodeBlockFooter, CodeBlockHeader, CodeBlockLine, CodeBlockTitle,} from "@/components/ui/code-block"<CodeBlock> <CodeBlockHeader> <CodeBlockTitle>Terminal</CodeBlockTitle> <CodeBlockActions> <CodeBlockCopyButton value={output} /> </CodeBlockActions> </CodeBlockHeader> <CodeBlockContent>{output}</CodeBlockContent></CodeBlock>Use the following composition to build a CodeBlock:
CodeBlock├── CodeBlockHeader│ ├── CodeBlockTitle│ └── CodeBlockActions│ └── CodeBlockCopyButton├── CodeBlockActions (floating, when there is no header)├── CodeBlockContent│ └── CodeBlockLine└── CodeBlockFooterCodeBlockContent takes a string or a list of CodeBlockLine. Use lines when you need line variants such as added, removed, or highlighted.
Pass a string to CodeBlockContent. It renders as one text node, so it stays cheap while output streams.
$ pnpm build
> next build
✓ Compiled successfully in 4.2s
✓ Generating static pages (12/12)
Route (app) Size First Load JS
┌ ○ / 1.9 kB 112 kB
├ ○ /_not-found 993 B 103 kB
└ ƒ /api/chat 141 B 102 kBimport { CodeBlock, CodeBlockContent,} from "@/components/ui/code-block";
const output = `$ pnpm build> next build
✓ Compiled successfully in 4.2s ✓ Generating static pages (12/12)
Route (app) Size First Load JS┌ ○ / 1.9 kB 112 kB├ ○ /_not-found 993 B 103 kB└ ƒ /api/chat 141 B 102 kB`;
export function CodeBlockBasic() { return ( <div className="w-full max-w-md"> <CodeBlock> <CodeBlockContent>{output}</CodeBlockContent> </CodeBlock> </div> );}CodeBlockHeader holds a CodeBlockTitle and CodeBlockActions. CodeBlockCopyButton copies its value.
$ pnpm add ai @ai-sdk/react zod
Packages: +14
Progress: resolved 412, reused 398, downloaded 14, added 14, done
Done in 2.8s using pnpm v10.18.0import { TerminalIcon } from "lucide-react";import { CodeBlock, CodeBlockActions, CodeBlockContent, CodeBlockCopyButton, CodeBlockHeader, CodeBlockTitle,} from "@/components/ui/code-block";
const command = "pnpm add ai @ai-sdk/react zod";
const output = `$ ${command}Packages: +14Progress: resolved 412, reused 398, downloaded 14, added 14, doneDone in 2.8s using pnpm v10.18.0`;
export function CodeBlockHeaderExample() { return ( <div className="w-full max-w-md"> <CodeBlock> <CodeBlockHeader> <CodeBlockTitle> <TerminalIcon /> Terminal </CodeBlockTitle> <CodeBlockActions> <CodeBlockCopyButton value={output} /> </CodeBlockActions> </CodeBlockHeader> <CodeBlockContent>{output}</CodeBlockContent> </CodeBlock> </div> );}A long title truncates. To link the title to a file, wrap CodeBlockTitle in an anchor.
<CodeBlockHeader> <a href="/files/route.ts"> <CodeBlockTitle>app/api/chat/route.ts</CodeBlockTitle> </a></CodeBlockHeader>CodeBlockActions holds any number of buttons. For a wrap toggle, keep the state yourself and pass it to wrap.
$ pnpm lint
apps/web/components/message-list.tsx
18:22 error 'messages' is possibly 'undefined'. Add a guard before calling map
42:7 warning React Hook React.useEffect has a missing dependency: 'scrollToBottom'
✖ 2 problems (1 error, 1 warning)"use client";
import { DownloadIcon, TerminalIcon, WrapTextIcon } from "lucide-react";import * as React from "react";import { Button } from "@/components/ui/button";import { CodeBlock, CodeBlockActions, CodeBlockContent, CodeBlockCopyButton, CodeBlockHeader, CodeBlockTitle,} from "@/components/ui/code-block";
const output = `$ pnpm lintapps/web/components/message-list.tsx 18:22 error 'messages' is possibly 'undefined'. Add a guard before calling map 42:7 warning React Hook React.useEffect has a missing dependency: 'scrollToBottom'
✖ 2 problems (1 error, 1 warning)`;
function download(value: string, filename: string) { const url = URL.createObjectURL(new Blob([value], { type: "text/plain" })); const link = document.createElement("a"); link.href = url; link.download = filename; link.click(); URL.revokeObjectURL(url);}
export function CodeBlockActionsExample() { const [wrap, setWrap] = React.useState(true);
return ( <div className="w-full max-w-md"> <CodeBlock> <CodeBlockHeader> <CodeBlockTitle> <TerminalIcon /> Terminal </CodeBlockTitle> <CodeBlockActions> <Button variant="ghost" size="icon-xs" aria-label="Wrap lines" aria-pressed={wrap} className="aria-pressed:text-foreground" onClick={() => setWrap(!wrap)} > <WrapTextIcon /> </Button> <Button variant="ghost" size="icon-xs" aria-label="Download" onClick={() => download(output, "lint.log")} > <DownloadIcon /> </Button> <CodeBlockCopyButton value={output} /> </CodeBlockActions> </CodeBlockHeader> <CodeBlockContent wrap={wrap}>{output}</CodeBlockContent> </CodeBlock> </div> );}const [wrap, setWrap] = React.useState(false)
<CodeBlockActions> <Button variant="ghost" size="icon-xs" aria-label="Wrap lines" aria-pressed={wrap} onClick={() => setWrap(!wrap)} > <WrapTextIcon /> </Button> <CodeBlockCopyButton value={output} /></CodeBlockActions><CodeBlockContent wrap={wrap}>{output}</CodeBlockContent>Without a header, put CodeBlockActions directly inside CodeBlock. The actions float at the top end and stay hidden until the block is hovered or focused. They always show on touch screens.
$ pnpm add ai @ai-sdk/react zod
Progress: resolved 412, reused 398, downloaded 14, added 14, done
dependencies:
+ @ai-sdk/react 3.0.12
+ ai 6.0.12
+ zod 4.1.12
Done in 2.8s using pnpm v10.18.0import { CodeBlock, CodeBlockActions, CodeBlockContent, CodeBlockCopyButton,} from "@/components/ui/code-block";
const output = `$ pnpm add ai @ai-sdk/react zodProgress: resolved 412, reused 398, downloaded 14, added 14, donedependencies:+ @ai-sdk/react 3.0.12+ ai 6.0.12+ zod 4.1.12Done in 2.8s using pnpm v10.18.0`;
export function CodeBlockFloating() { return ( <div className="w-full max-w-md"> <CodeBlock> <CodeBlockActions> <CodeBlockCopyButton value={output} /> </CodeBlockActions> <CodeBlockContent>{output}</CodeBlockContent> </CodeBlock> </div> );}Add lineNumbers to CodeBlockContent. A string child is split into lines, and the numbers are not copied with a selection.
{ "name": "chat", "private": true, "scripts": { "dev": "next dev", "build": "next build" }}import { CodeBlock, CodeBlockContent,} from "@/components/ui/code-block";
const code = `{ "name": "chat", "private": true, "scripts": { "dev": "next dev", "build": "next build" }}`;
export function CodeBlockLineNumbers() { return ( <div className="w-full max-w-md"> <CodeBlock> <CodeBlockContent lineNumbers>{code}</CodeBlockContent> </CodeBlock> </div> );}Set startLine to number lines from a given line, for a snippet from the middle of a file. The gutter grows to fit the widest number.
if (!session) { redirect("/login") }
const projects = await db.project.findMany({ where: { ownerId: session.user.id }, orderBy: { updatedAt: "desc" }, })
return <ProjectList projects={projects} />}"use client";
import { useHighlightedCode } from "@/hooks/use-highlighted-code";import { CodeBlock, CodeBlockContent, CodeBlockHeader, CodeBlockLine, CodeBlockTitle,} from "@/components/ui/code-block";
const code = ` if (!session) { redirect("/login") }
const projects = await db.project.findMany({ where: { ownerId: session.user.id }, orderBy: { updatedAt: "desc" }, })
return <ProjectList projects={projects} />}`;
export function CodeBlockStartLine() { const lines = useHighlightedCode(code, "tsx");
return ( <div className="w-full max-w-md"> <CodeBlock> <CodeBlockHeader> <CodeBlockTitle>app/dashboard/page.tsx</CodeBlockTitle> </CodeBlockHeader> <CodeBlockContent lineNumbers startLine={995}> {lines.map((line, index) => ( <CodeBlockLine key={index}>{line}</CodeBlockLine> ))} </CodeBlockContent> </CodeBlock> </div> );}Long lines scroll by default. Add wrap to wrap them, which suits tool output and error messages. To let the reader toggle it, see Actions.
Error: The model "anthropic/claude-sonnet-4.5" returned an error: Request too large. The prompt is 212,431 tokens and the context window is 200,000 tokens. Remove some messages or summarize the conversation and try again.Error: The model "anthropic/claude-sonnet-4.5" returned an error: Request too large. The prompt is 212,431 tokens and the context window is 200,000 tokens. Remove some messages or summarize the conversation and try again.import { CodeBlock, CodeBlockContent,} from "@/components/ui/code-block";
const output = `Error: The model "anthropic/claude-sonnet-4.5" returned an error: Request too large. The prompt is 212,431 tokens and the context window is 200,000 tokens. Remove some messages or summarize the conversation and try again.`;
export function CodeBlockWrap() { return ( <div className="flex w-full max-w-md flex-col gap-4"> <CodeBlock> <CodeBlockContent>{output}</CodeBlockContent> </CodeBlock> <CodeBlock> <CodeBlockContent wrap>{output}</CodeBlockContent> </CodeBlock> </div> );}Render each line as a CodeBlockLine and set variant="highlighted" on the lines to call out.
export { experimental: { reactCompiler: true, viewTransition: true, }, images: { remotePatterns: [{ hostname: "avatars.githubusercontent.com" }], },}import { CodeBlock, CodeBlockContent, CodeBlockLine,} from "@/components/ui/code-block";
const code = `export { experimental: { reactCompiler: true, viewTransition: true, }, images: { remotePatterns: [{ hostname: "avatars.githubusercontent.com" }], },}`;
const highlighted = [2, 3];
export function CodeBlockHighlighted() { return ( <div className="w-full max-w-md"> <CodeBlock> <CodeBlockContent lineNumbers> {code.split("\n").map((line, index) => ( <CodeBlockLine key={index} variant={highlighted.includes(index) ? "highlighted" : "default"} > {line} </CodeBlockLine> ))} </CodeBlockContent> </CodeBlock> </div> );}Set variant="added" or variant="removed" on a CodeBlockLine. Each one adds a + or − marker.
export function Greeting({ name }: { name: string }) { return <p>Hello, {name}</p> return <p className="text-sm text-muted-foreground"> Hello, {name}! </p>}import { CodeBlock, CodeBlockActions, CodeBlockContent, CodeBlockCopyButton, CodeBlockHeader, CodeBlockLine, CodeBlockTitle,} from "@/components/ui/code-block";
const patch = ` export function Greeting({ name }: { name: string }) {- return <p>Hello, {name}</p>+ return <p className="text-sm text-muted-foreground">+ Hello, {name}!+ </p> }`;
function getVariant(line: string) { if (line.startsWith("+")) { return "added" as const; }
if (line.startsWith("-")) { return "removed" as const; }
return "default" as const;}
export function CodeBlockDiff() { return ( <div className="w-full max-w-md"> <CodeBlock> <CodeBlockHeader> <CodeBlockTitle>components/greeting.tsx</CodeBlockTitle> <CodeBlockActions> <CodeBlockCopyButton value={patch} /> </CodeBlockActions> </CodeBlockHeader> <CodeBlockContent> {patch.split("\n").map((line, index) => ( <CodeBlockLine key={index} variant={getVariant(line)}> {line.slice(1)} </CodeBlockLine> ))} </CodeBlockContent> </CodeBlock> </div> );}Cap the height with a max-h-* class on CodeBlockContent. The content scrolls inside the block.
[01] GET /api/chat 200 in 12ms
[02] GET /api/chat 200 in 19ms
[03] GET /api/chat 200 in 26ms
[04] GET /api/chat 200 in 33ms
[05] GET /api/chat 200 in 40ms
[06] GET /api/chat 200 in 47ms
[07] GET /api/chat 200 in 14ms
[08] GET /api/chat 200 in 21ms
[09] GET /api/chat 200 in 28ms
[10] GET /api/chat 200 in 35ms
[11] GET /api/chat 200 in 42ms
[12] GET /api/chat 200 in 49ms
[13] GET /api/chat 200 in 16ms
[14] GET /api/chat 200 in 23ms
[15] GET /api/chat 200 in 30ms
[16] GET /api/chat 200 in 37ms
[17] GET /api/chat 200 in 44ms
[18] GET /api/chat 200 in 51ms
[19] GET /api/chat 200 in 18ms
[20] GET /api/chat 200 in 25ms
[21] GET /api/chat 200 in 32ms
[22] GET /api/chat 200 in 39ms
[23] GET /api/chat 200 in 46ms
[24] GET /api/chat 200 in 13ms
[25] GET /api/chat 200 in 20ms
[26] GET /api/chat 200 in 27ms
[27] GET /api/chat 200 in 34ms
[28] GET /api/chat 200 in 41ms
[29] GET /api/chat 200 in 48ms
[30] GET /api/chat 200 in 15ms
[31] GET /api/chat 200 in 22ms
[32] GET /api/chat 200 in 29ms
[33] GET /api/chat 200 in 36ms
[34] GET /api/chat 200 in 43ms
[35] GET /api/chat 200 in 50ms
[36] GET /api/chat 200 in 17ms
[37] GET /api/chat 200 in 24ms
[38] GET /api/chat 200 in 31ms
[39] GET /api/chat 200 in 38ms
[40] GET /api/chat 200 in 45msimport { CodeBlock, CodeBlockActions, CodeBlockContent, CodeBlockCopyButton, CodeBlockHeader, CodeBlockTitle,} from "@/components/ui/code-block";
const logs = Array.from( { length: 40 }, (_, index) => `[${String(index + 1).padStart(2, "0")}] GET /api/chat 200 in ${12 + ((index * 7) % 40)}ms`,).join("\n");
export function CodeBlockHeight() { return ( <div className="w-full max-w-md"> <CodeBlock> <CodeBlockHeader> <CodeBlockTitle>Logs</CodeBlockTitle> <CodeBlockActions> <CodeBlockCopyButton value={logs} /> </CodeBlockActions> </CodeBlockHeader> <CodeBlockContent className="max-h-48">{logs}</CodeBlockContent> </CodeBlock> </div> );}CodeBlockFooter is a slot below the content. For Show More, cap the content while it is collapsed and put a ghost xs Button in the footer.
"use client"
import * as React from "react"import { useChat } from "@ai-sdk/react"
import { Button } from "@/components/ui/button"import { Input } from "@/components/ui/input"
export function Chat() { const [input, setInput] = React.useState("") const { messages, sendMessage, status } = useChat()
return ( <div className="flex flex-col gap-4"> {messages.map((message) => ( <div key={message.id}> {message.parts.map((part, index) => part.type === "text" ? <p key={index}>{part.text}</p> : null )} </div> ))} <form onSubmit={(event) => { event.preventDefault() sendMessage({ text: input }) setInput("") }} > <Input value={input} onChange={(event) => setInput(event.target.value)} /> <Button disabled={status !== "ready"}>Send</Button> </form> </div> )}"use client";
import { ChevronDownIcon } from "lucide-react";import * as React from "react";import { useHighlightedCode } from "@/hooks/use-highlighted-code";import { Button } from "@/components/ui/button";import { CodeBlock, CodeBlockActions, CodeBlockContent, CodeBlockCopyButton, CodeBlockFooter, CodeBlockHeader, CodeBlockLine, CodeBlockTitle,} from "@/components/ui/code-block";
const code = `"use client"
import * as React from "react"import { useChat } from "@ai-sdk/react"
import { Button } from "@/components/ui/button"import { Input } from "@/components/ui/input"
export function Chat() { const [input, setInput] = React.useState("") const { messages, sendMessage, status } = useChat()
return ( <div className="flex flex-col gap-4"> {messages.map((message) => ( <div key={message.id}> {message.parts.map((part, index) => part.type === "text" ? <p key={index}>{part.text}</p> : null )} </div> ))} <form onSubmit={(event) => { event.preventDefault() sendMessage({ text: input }) setInput("") }} > <Input value={input} onChange={(event) => setInput(event.target.value)} /> <Button disabled={status !== "ready"}>Send</Button> </form> </div> )}`;
export function CodeBlockShowMore() { const [expanded, setExpanded] = React.useState(false); const lines = useHighlightedCode(code, "tsx"); const id = React.useId();
return ( <div className="w-full max-w-md"> <CodeBlock> <CodeBlockHeader> <CodeBlockTitle>components/chat.tsx</CodeBlockTitle> <CodeBlockActions> <CodeBlockCopyButton value={code} /> </CodeBlockActions> </CodeBlockHeader> <CodeBlockContent id={id} lineNumbers className={expanded ? undefined : "max-h-64 overflow-y-hidden"} > {lines.map((line, index) => ( <CodeBlockLine key={index}>{line}</CodeBlockLine> ))} </CodeBlockContent> <CodeBlockFooter className="justify-center"> <Button variant="ghost" size="xs" aria-expanded={expanded} aria-controls={id} onClick={() => setExpanded(!expanded)} > <ChevronDownIcon className="in-aria-expanded:rotate-180 transition-transform" /> {expanded ? "Show less" : "Show more"} </Button> </CodeBlockFooter> </CodeBlock> </div> );}const [expanded, setExpanded] = React.useState(false)const id = React.useId()
<CodeBlockContent id={id} className={expanded ? undefined : "max-h-64 overflow-y-hidden"}> {code}</CodeBlockContent><CodeBlockFooter className="justify-center"> <Button variant="ghost" size="xs" aria-expanded={expanded} aria-controls={id} onClick={() => setExpanded(!expanded)} > {expanded ? "Show less" : "Show more"} </Button></CodeBlockFooter>Use size="sm" for tool output. It has tighter padding and text-xs text.
$ pnpm add ai @ai-sdk/react zod
Progress: resolved 412, reused 398, downloaded 14, added 14, done
Done in 2.8s using pnpm v10.18.0$ pnpm add ai @ai-sdk/react zod
Progress: resolved 412, reused 398, downloaded 14, added 14, done
Done in 2.8s using pnpm v10.18.0import { TerminalIcon } from "lucide-react";import { CodeBlock, CodeBlockActions, CodeBlockContent, CodeBlockCopyButton, CodeBlockHeader, CodeBlockTitle,} from "@/components/ui/code-block";
const output = `$ pnpm add ai @ai-sdk/react zodProgress: resolved 412, reused 398, downloaded 14, added 14, doneDone in 2.8s using pnpm v10.18.0`;
export function CodeBlockSize() { return ( <div className="flex w-full max-w-md flex-col gap-4"> <CodeBlock> <CodeBlockHeader> <CodeBlockTitle> <TerminalIcon /> Terminal </CodeBlockTitle> <CodeBlockActions> <CodeBlockCopyButton value={output} /> </CodeBlockActions> </CodeBlockHeader> <CodeBlockContent wrap>{output}</CodeBlockContent> </CodeBlock> <CodeBlock size="sm"> <CodeBlockHeader> <CodeBlockTitle> <TerminalIcon /> Terminal </CodeBlockTitle> <CodeBlockActions> <CodeBlockCopyButton value={output} /> </CodeBlockActions> </CodeBlockHeader> <CodeBlockContent wrap>{output}</CodeBlockContent> </CodeBlock> </div> );}Use variant="destructive" for errors and failed output.
TypeError: Cannot read properties of undefined (reading 'map')
at MessageList (components/message-list.tsx:18:22)
at renderWithHooks (react-dom.development.js:16305:18)import { CodeBlock, CodeBlockActions, CodeBlockContent, CodeBlockCopyButton, CodeBlockHeader, CodeBlockTitle,} from "@/components/ui/code-block";
const error = `TypeError: Cannot read properties of undefined (reading 'map') at MessageList (components/message-list.tsx:18:22) at renderWithHooks (react-dom.development.js:16305:18)`;
export function CodeBlockDestructive() { return ( <div className="w-full max-w-md"> <CodeBlock variant="destructive"> <CodeBlockHeader> <CodeBlockTitle>Error</CodeBlockTitle> <CodeBlockActions> <CodeBlockCopyButton value={error} /> </CodeBlockActions> </CodeBlockHeader> <CodeBlockContent wrap>{error}</CodeBlockContent> </CodeBlock> </div> );}Put a CodeBlock with size="sm" in TraceCollapsibleContent for tool and terminal output. Add wrap and a max-h-64 class to CodeBlockContent so long output wraps and scrolls inside the trace. The block sets its own text color, so TraceCollapsibleContent does not mute it.
$ node --version
v24.18.0$ pnpm dev
Error: listen EADDRINUSE: address already in use :::3000
at Server.setupListenHandle [as _listen2] (node:net:1908:16)import { TerminalIcon } from "lucide-react";import { CodeBlock, CodeBlockContent,} from "@/components/ui/code-block";import { Trace, TraceCollapsible, TraceCollapsibleContent, TraceCollapsibleTrigger, TraceIcon, TraceLine, TraceTitle,} from "@/components/ui/trace";
const output = `$ node --versionv24.18.0`;
const error = `$ pnpm devError: listen EADDRINUSE: address already in use :::3000 at Server.setupListenHandle [as _listen2] (node:net:1908:16)`;
export function CodeBlockTrace() { return ( <div className="w-full max-w-md"> <Trace> <TraceCollapsible defaultOpen> <TraceCollapsibleTrigger> <TraceLine> <TraceIcon> <TerminalIcon /> </TraceIcon> <TraceTitle>Ran node --version</TraceTitle> </TraceLine> </TraceCollapsibleTrigger> <TraceCollapsibleContent> <CodeBlock size="sm"> <CodeBlockContent wrap className="max-h-64"> {output} </CodeBlockContent> </CodeBlock> </TraceCollapsibleContent> </TraceCollapsible> <TraceCollapsible defaultOpen> <TraceCollapsibleTrigger> <TraceLine status="error"> <TraceIcon> <TerminalIcon /> </TraceIcon> <TraceTitle>Failed to run pnpm dev</TraceTitle> </TraceLine> </TraceCollapsibleTrigger> <TraceCollapsibleContent> <CodeBlock size="sm" variant="destructive"> <CodeBlockContent wrap className="max-h-64"> {error} </CodeBlockContent> </CodeBlock> </TraceCollapsibleContent> </TraceCollapsible> </Trace> </div> );}Show several files with Tabs, one CodeBlock in each TabsContent.
import { streamText } from "ai"
export async function POST(request: Request) { const { messages } = await request.json() const result = streamText({ model: "anthropic/claude-sonnet-4.5", messages })
return result.toUIMessageStreamResponse()}"use client";
import { useHighlightedCode } from "@/hooks/use-highlighted-code";import { CodeBlock, CodeBlockActions, CodeBlockContent, CodeBlockCopyButton, CodeBlockHeader, CodeBlockLine, CodeBlockTitle,} from "@/components/ui/code-block";import { Tabs, TabsContent, TabsList, TabsTrigger,} from "@/components/ui/tabs";
const files = [ { id: "route", title: "app/api/chat/route.ts", language: "ts", code: `import { streamText } from "ai"
export async function POST(request: Request) { const { messages } = await request.json() const result = streamText({ model: "anthropic/claude-sonnet-4.5", messages })
return result.toUIMessageStreamResponse()}`, }, { id: "chat", title: "components/chat.tsx", language: "tsx", code: `"use client"
import { useChat } from "@ai-sdk/react"
export function Chat() { const { messages } = useChat()
return <MessageList messages={messages} />}`, }, { id: "env", title: ".env.local", language: "bash", code: "AI_GATEWAY_API_KEY=your-api-key", },];
function FileCode({ code, language }: { code: string; language: string }) { const lines = useHighlightedCode(code, language);
return ( <CodeBlockContent lineNumbers> {lines.map((line, index) => ( <CodeBlockLine key={index}>{line}</CodeBlockLine> ))} </CodeBlockContent> );}
export function CodeBlockTabs() { return ( <Tabs defaultValue="route" className="w-full max-w-md"> <TabsList> {files.map((file) => ( <TabsTrigger key={file.id} value={file.id}> {file.title.split("/").at(-1)} </TabsTrigger> ))} </TabsList> {files.map((file) => ( <TabsContent key={file.id} value={file.id}> <CodeBlock> <CodeBlockHeader> <CodeBlockTitle>{file.title}</CodeBlockTitle> <CodeBlockActions> <CodeBlockCopyButton value={file.code} /> </CodeBlockActions> </CodeBlockHeader> <FileCode code={file.code} language={file.language} /> </CodeBlock> </TabsContent> ))} </Tabs> );}Put a CodeBlock between paragraphs inside a Message.
Create a route handler that returns the stream:
import { streamText } from "ai"
export async function POST(request: Request) { const { messages } = await request.json()
const result = streamText({ model: "anthropic/claude-sonnet-4.5", messages, })
return result.toUIMessageStreamResponse()}The client reads it with useChat.
"use client";
import { useHighlightedCode } from "@/hooks/use-highlighted-code";import { Bubble, BubbleContent } from "@/components/ui/bubble";import { CodeBlock, CodeBlockActions, CodeBlockContent, CodeBlockCopyButton, CodeBlockHeader, CodeBlockLine, CodeBlockTitle,} from "@/components/ui/code-block";import { Message, MessageContent } from "@/components/ui/message";
const code = `import { streamText } from "ai"
export async function POST(request: Request) { const { messages } = await request.json()
const result = streamText({ model: "anthropic/claude-sonnet-4.5", messages, })
return result.toUIMessageStreamResponse()}`;
export function CodeBlockMessage() { const lines = useHighlightedCode(code, "ts");
return ( <div className="flex w-full max-w-md flex-col gap-4 text-sm"> <Message align="end"> <MessageContent> <Bubble align="end" variant="muted"> <BubbleContent>How do I stream a response?</BubbleContent> </Bubble> </MessageContent> </Message> <Message> <MessageContent className="gap-3"> <p>Create a route handler that returns the stream:</p> <CodeBlock> <CodeBlockHeader> <CodeBlockTitle>app/api/chat/route.ts</CodeBlockTitle> <CodeBlockActions> <CodeBlockCopyButton value={code} /> </CodeBlockActions> </CodeBlockHeader> <CodeBlockContent> {lines.map((line, index) => ( <CodeBlockLine key={index}>{line}</CodeBlockLine> ))} </CodeBlockContent> </CodeBlock> <p>The client reads it with useChat.</p> </MessageContent> </Message> </div> );}To enable RTL support in shadcn/ui, see the RTL configuration guide. Code always reads left to right. The header follows the page direction.
ثبّت الحزم، ثم شغّل خادم التطوير.
pnpm add ai @ai-sdk/react zodpnpm dev"use client";
import { TerminalIcon } from "lucide-react";import { type Translations, useTranslation,} from "@/components/language-selector";import { useHighlightedCode } from "@/hooks/use-highlighted-code";import { CodeBlock, CodeBlockActions, CodeBlockContent, CodeBlockCopyButton, CodeBlockHeader, CodeBlockLine, CodeBlockTitle,} from "@/components/ui/code-block";
const translations: Translations = { en: { dir: "ltr", values: { description: "Install the packages, then start the dev server.", title: "Terminal", }, }, ar: { dir: "rtl", values: { description: "ثبّت الحزم، ثم شغّل خادم التطوير.", title: "الطرفية", }, }, he: { dir: "rtl", values: { description: "התקן את החבילות, ואז הפעל את שרת הפיתוח.", title: "מסוף", }, },};
const code = `pnpm add ai @ai-sdk/react zodpnpm dev`;
export function CodeBlockRtl() { const { dir, language, t } = useTranslation(translations, "ar"); const lines = useHighlightedCode(code, "bash");
return ( <div className="flex w-full max-w-md flex-col gap-3 text-sm" lang={language} dir={dir} > <p>{t.description}</p> <CodeBlock> <CodeBlockHeader> <CodeBlockTitle> <TerminalIcon /> {t.title} </CodeBlockTitle> <CodeBlockActions> <CodeBlockCopyButton value={code} /> </CodeBlockActions> </CodeBlockHeader> <CodeBlockContent lineNumbers> {lines.map((line, index) => ( <CodeBlockLine key={index}>{line}</CodeBlockLine> ))} </CodeBlockContent> </CodeBlock> </div> );}useHighlightedCode highlights with Shiki and returns one React node per line for CodeBlockLine.
{ "location": "San Francisco", "temperature": 18, "unit": "celsius", "conditions": ["fog", "wind"]}"use client";
import { useHighlightedCode } from "@/hooks/use-highlighted-code";import { CodeBlock, CodeBlockContent, CodeBlockHeader, CodeBlockLine, CodeBlockTitle,} from "@/components/ui/code-block";
const code = `{ "location": "San Francisco", "temperature": 18, "unit": "celsius", "conditions": ["fog", "wind"]}`;
export function CodeBlockHighlighting() { const lines = useHighlightedCode(code, "json");
return ( <div className="w-full max-w-md"> <CodeBlock> <CodeBlockHeader> <CodeBlockTitle>getWeather output</CodeBlockTitle> </CodeBlockHeader> <CodeBlockContent> {lines.map((line, index) => ( <CodeBlockLine key={index}>{line}</CodeBlockLine> ))} </CodeBlockContent> </CodeBlock> </div> );}const lines = useHighlightedCode(code, "json")
<CodeBlock> <CodeBlockContent> {lines.map((line, index) => ( <CodeBlockLine key={index}>{line}</CodeBlockLine> ))} </CodeBlockContent></CodeBlock>Shiki uses its JavaScript regex engine, so there is no WASM to load.
Each language loads the first time it is used. Edit the list in lib/highlighter.tsx. A language that is not in the list renders as plain text.
@import "tailwindcss";
:root { --radius: 0.625rem;}def greet(name: str) -> str: return f"Hello, {name}!"A language that is not in the list renders as plain text."use client";
import { useHighlightedCode } from "@/hooks/use-highlighted-code";import { CodeBlock, CodeBlockContent, CodeBlockHeader, CodeBlockLine, CodeBlockTitle,} from "@/components/ui/code-block";
const samples = [ { language: "css", title: "app/globals.css", code: `@import "tailwindcss";
:root { --radius: 0.625rem;}`, }, { language: "python", title: "main.py", code: `def greet(name: str) -> str: return f"Hello, {name}!"`, }, { language: "text", title: "notes.txt", code: "A language that is not in the list renders as plain text.", },];
function Sample({ code, language }: { code: string; language: string }) { const lines = useHighlightedCode(code, language);
return ( <CodeBlockContent> {lines.map((line, index) => ( <CodeBlockLine key={index}>{line}</CodeBlockLine> ))} </CodeBlockContent> );}
export function CodeBlockLanguages() { return ( <div className="flex w-full max-w-md flex-col gap-4"> {samples.map((sample) => ( <CodeBlock key={sample.language}> <CodeBlockHeader> <CodeBlockTitle>{sample.title}</CodeBlockTitle> </CodeBlockHeader> <Sample code={sample.code} language={sample.language} /> </CodeBlock> ))} </div> );}The list ships with bash, css, diff, html, javascript, json, jsx, markdown, python, sql, tsx, typescript and yaml, plus aliases such as ts, js, sh and yml.
Pass "ansi" as the language to render the colors in terminal output. Without it, escape codes show as text.
$ pnpm test[32m✓[39m src/format.test.ts [2m(4 tests)[22m[32m✓[39m src/parse.test.ts [2m(7 tests)[22m[31m✗[39m src/stream.test.ts [2m(3 tests | 1 failed)[22m
[31mAssertionError: expected 'hello' to be 'hello world'[39m
Tests [31m1 failed[39m | [32m13 passed[39m (14)"use client";
import { TerminalIcon } from "lucide-react";import { useHighlightedCode } from "@/hooks/use-highlighted-code";import { CodeBlock, CodeBlockActions, CodeBlockContent, CodeBlockCopyButton, CodeBlockHeader, CodeBlockLine, CodeBlockTitle,} from "@/components/ui/code-block";
const output = `$ pnpm test\x1b[32m✓\x1b[39m src/format.test.ts \x1b[2m(4 tests)\x1b[22m\x1b[32m✓\x1b[39m src/parse.test.ts \x1b[2m(7 tests)\x1b[22m\x1b[31m✗\x1b[39m src/stream.test.ts \x1b[2m(3 tests | 1 failed)\x1b[22m
\x1b[31mAssertionError: expected 'hello' to be 'hello world'\x1b[39m
Tests \x1b[31m1 failed\x1b[39m | \x1b[32m13 passed\x1b[39m (14)`;
export function CodeBlockAnsi() { const lines = useHighlightedCode(output, "ansi");
return ( <div className="w-full max-w-md"> <CodeBlock> <CodeBlockHeader> <CodeBlockTitle> <TerminalIcon /> Terminal </CodeBlockTitle> <CodeBlockActions> {/* biome-ignore lint/suspicious/noControlCharactersInRegex: matches ANSI escape sequences */} <CodeBlockCopyButton value={output.replace(/\x1b\[[\d;]*m/g, "")} /> </CodeBlockActions> </CodeBlockHeader> <CodeBlockContent wrap> {lines.map((line, index) => ( <CodeBlockLine key={index}>{line}</CodeBlockLine> ))} </CodeBlockContent> </CodeBlock> </div> );}ANSI colors do not carry from one line to the next.
highlightCode is async and works in a Server Component, so Shiki stays out of the client bundle.
import { highlightCode } from "@/lib/highlighter"
export async function ServerCode({ code }: { code: string }) { const lines = (await highlightCode(code, "tsx")) ?? code.split("\n")
return ( <CodeBlock> <CodeBlockContent lineNumbers> {lines.map((line, index) => ( <CodeBlockLine key={index}>{line}</CodeBlockLine> ))} </CodeBlockContent> </CodeBlock> )}CodeBlockContent is a pre with a code element and always sets dir="ltr", so code reads left to right on an RTL page.CodeBlockContent has tabIndex={0}, so keyboard users can focus it and scroll it. Pass tabIndex={-1} to remove the tab stop.+ or − marker as well as a tint, so the change does not rely on color.CodeBlockCopyButton is labelled "Copy", and a polite live region announces "Copied" after a successful copy.aria-pressed so its state is announced.aria-expanded and point aria-controls at the id of CodeBlockContent.The surface that holds the header, the content and the footer.
| Prop | Type | Default |
|---|---|---|
| variant | "default" | "destructive" | "default" |
The tone of the block. Use `destructive` for errors. | ||
| size | "default" | "sm" | "default" |
`sm` has tighter padding and `text-xs` code, for tool output. | ||
| className | string | — |
Additional classes to apply to the block. | ||
A row above the content for a title and actions.
| Prop | Type | Default |
|---|---|---|
| className | string | — |
Additional classes to apply to the header. | ||
A filename or a label, with an optional icon. It truncates.
| Prop | Type | Default |
|---|---|---|
| className | string | — |
Additional classes to apply to the title. | ||
Holds buttons at the end of the header. As a direct child of CodeBlock, it floats.
| Prop | Type | Default |
|---|---|---|
| className | string | — |
Additional classes to apply to the actions. | ||
A ghost icon button that copies value to the clipboard. It accepts the props of Button.
| Prop | Type | Default |
|---|---|---|
| value* | string | — |
The text to copy. | ||
| className | string | — |
Additional classes to apply to the button. | ||
The pre that scrolls or wraps.
| Prop | Type | Default |
|---|---|---|
| wrap | boolean | false |
Wrap long lines instead of scrolling. | ||
| lineNumbers | boolean | false |
Number each line. A string child is split into lines. | ||
| startLine | number | 1 |
The number of the first line, when `lineNumbers` is set. | ||
| className | string | — |
Additional classes to apply to the content, such as `max-h-48`. | ||
One line of code.
| Prop | Type | Default |
|---|---|---|
| variant | "default" | "added" | "removed" | "highlighted" | "default" |
The state of the line. | ||
| className | string | — |
Additional classes to apply to the line. | ||
A row below the content, such as for a Show More button.
| Prop | Type | Default |
|---|---|---|
| className | string | — |
Additional classes to apply to the footer. | ||
const lines = useHighlightedCode(code, language)Returns one React node per line. It returns plain lines until the first pass finishes, and for a language that is not in the list.
| Prop | Type | Default |
|---|---|---|
| code* | string | — |
The code to highlight. A trailing newline is ignored. | ||
| language* | string | — |
A language or alias from `lib/highlighter.tsx`, or `"ansi"`. | ||
On This Page
InstallationUsageCompositionExamplesBasicHeaderActionsFloating actionsLine numbersStart lineWrapHighlighted linesDiffHeightShow moreSizeDestructiveTraceTabsMessageRTLSyntax HighlightingLanguagesTerminal outputServer renderingAccessibilityAPI ReferenceCodeBlockCodeBlockHeaderCodeBlockTitleCodeBlockActionsCodeBlockCopyButtonCodeBlockContentCodeBlockLineCodeBlockFooteruseHighlightedCode