Code Block
Code in a tray, with a title, actions, line numbers and diffs.
import { streamText } from "ai";
export async function POST(request: Request) { const { messages } = await request.json(); const result = streamText({ model: "anthropic/claude-sonnet-5.5", messages }); return result.toUIMessageStreamResponse();}<CodeBlock> <CodeBlockHeader> <HugeiconsIcon icon={SourceCodeIcon} /> <CodeBlockTitle>app/api/chat/route.ts</CodeBlockTitle> <CodeBlockActions> <CopyButton aria-label="Copy code" value={code} /> </CodeBlockActions> </CodeBlockHeader> <CodeBlockContent lineNumbers>{lines}</CodeBlockContent></CodeBlock>Installation
$ npx shadcn@latest add @constella/code-blockUsage
import { CodeBlock, CodeBlockActions, CodeBlockContent,} from "@/components/ai/code-block";import { CopyButton } from "@/components/primitives/copy-button";<CodeBlock> <CodeBlockContent>{code}</CodeBlockContent> <CodeBlockActions> <CopyButton value={code} /> </CodeBlockActions></CodeBlock>Composition
CodeBlock├── CodeBlockHeader│ ├── Icon│ ├── CodeBlockTitle│ └── CodeBlockActions├── CodeBlockContent│ └── CodeBlockLine├── CodeBlockActions (without a header)└── CodeBlockFooterCodeBlockContent takes a string, or a CodeBlockLine per line. Your app highlights the code: render its tokens inside each line, and add your highlighter's theme CSS. The content always reads left to right, and copying a selection leaves out line numbers and diff marks.
Code Block Actions
Put CodeBlockActions straight in the CodeBlock, with no header, and it sits on the first line.
pnpm add ai @ai-sdk/react zod<CodeBlock> <CodeBlockContent>pnpm add ai @ai-sdk/react zod</CodeBlockContent> <CodeBlockActions> <CopyButton aria-label="Copy command" value={command} /> </CodeBlockActions></CodeBlock>Wrap
Set wrap on CodeBlockContent to wrap long lines instead of scrolling. For a toggle, keep the state yourself.
$ pnpm lint
apps/web/components/message-list.tsx
18:22 error 'messages' is possibly 'undefined'. Add a guard before calling map no-unsafe-call
42:7 warning React Hook useEffect has a missing dependency: 'scrollToEnd' react-hooks/exhaustive-deps
✖ 2 problems (1 error, 1 warning)const [wrap, setWrap] = useState(true);
<CodeBlockActions> <Toggle aria-label="Wrap lines" pressed={wrap} onPressedChange={setWrap} size="icon-sm"> <HugeiconsIcon icon={TextWrapIcon} /> </Toggle> <CopyButton aria-label="Copy output" value={output} /></CodeBlockActions><CodeBlockContent wrap={wrap}>{output}</CodeBlockContent>Line Numbers
Set lineNumbers on CodeBlockContent. Use startLine when the code is an excerpt.
export async function GET() { const user = await getUser(); if (!user) { return new Response("Unauthorized", { status: 401 }); } return Response.json(user);}<CodeBlockContent lineNumbers startLine={18}> {lines}</CodeBlockContent>Code Block Line
Use the variant prop on CodeBlockLine to mark a line.
| Variant | Use |
|---|---|
default | A plain line |
highlighted | A line to point at |
added | A line a change adds, marked + |
removed | A line a change removes, marked − |
Highlighted
export async function GET() { const user = await getUser(); if (!user) { return new Response("Unauthorized", { status: 401 }); } return Response.json(user);}<CodeBlockContent lineNumbers> <CodeBlockLine>{"export async function GET() {"}</CodeBlockLine> <CodeBlockLine>{" const user = await getUser();"}</CodeBlockLine> <CodeBlockLine variant="highlighted">{" if (!user) {"}</CodeBlockLine></CodeBlockContent>Diff
export function Greeting({ name }: { name: string }) { return <p>Hello, {name}</p>; return ( <p className="greeting">Hello, {name}!</p> );}<CodeBlockContent> <CodeBlockLine variant="removed"> {" return <p>Hello, {name}</p>;"} </CodeBlockLine> <CodeBlockLine variant="added">{" return ("}</CodeBlockLine></CodeBlockContent>Size
Use the size prop on CodeBlock: sm or default.
| Size | Text | Padding |
|---|---|---|
sm | 12px | space.sm × space.xs |
default | 13px | space.md × space.sm |
pnpm add ai @ai-sdk/react zodpnpm add ai @ai-sdk/react zod<CodeBlock size="sm"> <CodeBlockContent>{command}</CodeBlockContent> <CodeBlockActions> <CopyButton aria-label="Copy command" size="icon-xs" value={command} /> </CodeBlockActions></CodeBlock>Color
Use color="danger" for an error's output. CodeBlockFooter holds a line under the code.
Error: Cannot find module '@/lib/ai'
at resolve (node:internal/modules/esm/resolve:873:18)
at app/api/chat/route.ts:1:24<CodeBlock color="danger"> <CodeBlockHeader> <HugeiconsIcon icon={Alert02Icon} /> <CodeBlockTitle>Build failed</CodeBlockTitle> </CodeBlockHeader> <CodeBlockContent>{error}</CodeBlockContent> <CodeBlockFooter>Exited with code 1 after 2.4s</CodeBlockFooter></CodeBlock>Markdown
Markdown renders its fenced code in a CodeBlock, with a copy button on the first line.
API Reference
Every part takes sx, applied last.
| Part | Adds |
|---|---|
CodeBlock | color: danger; size: sm or default |
CodeBlockContent | lineNumbers; startLine (default 1); wrap |
CodeBlockLine | variant: default, highlighted, added or removed |
CodeBlockTitle | render, to make it a heading: render={<h3 />} |
CodeBlockHeader, CodeBlockActions, CodeBlockFooter | Nothing; a div each |