Code Block

Code in a tray, with a title, actions, line numbers and diffs.

View as Markdown
app/api/chat/route.ts
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-block

Usage

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)└── CodeBlockFooter

CodeBlockContent 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.

Terminal
$ 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.

VariantUse
defaultA plain line
highlightedA line to point at
addedA line a change adds, marked +
removedA 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

components/greeting.tsx
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.

SizeTextPadding
sm12pxspace.sm × space.xs
default13pxspace.md × space.sm
pnpm add ai @ai-sdk/react zod
pnpm 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.

Build failed
Error: Cannot find module '@/lib/ai'
    at resolve (node:internal/modules/esm/resolve:873:18)
    at app/api/chat/route.ts:1:24
Exited with code 1 after 2.4s
<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.

PartAdds
CodeBlockcolor: danger; size: sm or default
CodeBlockContentlineNumbers; startLine (default 1); wrap
CodeBlockLinevariant: default, highlighted, added or removed
CodeBlockTitlerender, to make it a heading: render={<h3 />}
CodeBlockHeader, CodeBlockActions, CodeBlockFooterNothing; a div each