Sabil UI

Search documentation

Search for a command to run...

Components

Code Block

Displays a block of code with syntax highlighting and a copy button.

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-4.5",    system: "You are a helpful assistant. Answer in one short paragraph.",    messages,  })
  return result.toUIMessageStreamResponse()}

Installation#

  1. Install the dependencies

    npm install class-variance-authority cn lucide-react
  2. Copy the source into your project

    components/ui/code-block.tsx
    "use client"import * as React from "react"import { cva, type VariantProps } from "class-variance-authority"import { cn } from "cn"import { Button } from "@/components/ui/button"import { CheckIcon, CopyIcon } from "lucide-react"const codeBlockVariants = cva(  "group/code-block relative flex w-full min-w-0 flex-col overflow-hidden rounded-lg text-sm has-[>[data-slot=code-block-content]:focus-visible]:ring-3 has-[>[data-slot=code-block-content]:focus-visible]:ring-ring/50 data-[size=sm]:text-xs",  {    variants: {      variant: {        default:          "border border-input text-foreground has-[>[data-slot=code-block-content]:focus-visible]:border-ring dark:bg-input/30",        destructive:          "border border-destructive/30 text-destructive dark:border-destructive/40",      },    },    defaultVariants: {      variant: "default",    },  })function CodeBlock({  className,  variant = "default",  size = "default",  ...props}: React.ComponentProps<"div"> &  VariantProps<typeof codeBlockVariants> & { size?: "default" | "sm" }) {  return (    <div      data-slot="code-block"      data-variant={variant}      data-size={size}      className={cn(codeBlockVariants({ variant }), className)}      {...props}    />  )}function CodeBlockHeader({ className, ...props }: React.ComponentProps<"div">) {  return (    <div      data-slot="code-block-header"      className={cn(        "flex min-w-0 shrink-0 items-center gap-2 px-3 pt-3 text-muted-foreground group-data-[size=sm]/code-block:px-2.5 group-data-[size=sm]/code-block:pt-2.5 group-data-[variant=destructive]/code-block:text-destructive *:has-[>[data-slot=code-block-title]]:min-w-0",        className      )}      {...props}    />  )}function CodeBlockTitle({ className, ...props }: React.ComponentProps<"div">) {  return (    <div      data-slot="code-block-title"      className={cn(        "min-w-0 truncate font-sans text-sm font-medium text-foreground group-data-[size=sm]/code-block:text-xs group-data-[variant=destructive]/code-block:text-destructive [&_svg]:pointer-events-none [&_svg]:inline-block [&_svg]:align-middle [&_svg:not([class*='size-'])]:size-3.5 *:[svg]:me-1.5",        className      )}      {...props}    />  )}function CodeBlockActions({  className,  ...props}: React.ComponentProps<"div">) {  return (    <div      data-slot="code-block-actions"      className={cn(        "-my-1 ms-auto -me-1.5 flex shrink-0 items-center gap-1 [[data-slot=code-block]>&]:absolute [[data-slot=code-block]>&]:end-2 [[data-slot=code-block]>&]:top-2 [[data-slot=code-block]>&]:z-10 [[data-slot=code-block]>&]:m-0 [[data-slot=code-block]>&]:opacity-0 [[data-slot=code-block]>&]:transition-opacity [[data-slot=code-block]>&]:group-focus-within/code-block:opacity-100 [[data-slot=code-block]>&]:group-hover/code-block:opacity-100 group-data-[size=sm]/code-block:[[data-slot=code-block]>&]:end-1.5 group-data-[size=sm]/code-block:[[data-slot=code-block]>&]:top-1.5 [[data-slot=code-block]>&]:pointer-coarse:opacity-100 [[data-slot=code-block]>&]:*:[button]:bg-background [[data-slot=code-block]>&]:*:[button]:ring-1 [[data-slot=code-block]>&]:*:[button]:ring-foreground/10",        className      )}      {...props}    />  )}function CodeBlockCopyButton({  value,  variant = "ghost",  size = "icon-xs",  className,  onClick,  ...props}: React.ComponentProps<typeof Button> & { value: string }) {  const [copied, setCopied] = React.useState(false)  React.useEffect(() => {    if (!copied) {      return    }    const timeout = setTimeout(() => setCopied(false), 2000)    return () => clearTimeout(timeout)  }, [copied])  return (    <>      <Button        data-slot="code-block-copy-button"        data-copied={copied || undefined}        variant={variant}        size={size}        aria-label={copied ? "Copied" : "Copy"}        className={cn(className)}        onClick={(event) => {          onClick?.(event)          navigator.clipboard            .writeText(value)            .then(() => setCopied(true))            .catch(() => {})        }}        {...props}      >        {copied ? (          <CheckIcon          />        ) : (          <CopyIcon          />        )}      </Button>      <span aria-live="polite" className="sr-only">        {copied ? "Copied" : ""}      </span>    </>  )}function CodeBlockContent({  className,  children,  style,  wrap = false,  lineNumbers = false,  startLine = 1,  ...props}: React.ComponentProps<"pre"> & {  wrap?: boolean  lineNumbers?: boolean  startLine?: number}) {  return (    <pre      data-slot="code-block-content"      data-wrap={wrap || undefined}      data-numbered={lineNumbers || undefined}      dir="ltr"      tabIndex={0}      style={{ counterReset: `line ${startLine - 1}`, ...style }}      className={cn(        "min-h-0 min-w-0 scroll-fade-x scrollbar-thin overflow-auto px-3 py-3 text-start font-mono leading-relaxed outline-none group-has-data-[slot=code-block-footer]/code-block:pb-2 group-has-data-[slot=code-block-header]/code-block:pt-2 group-data-[size=sm]/code-block:px-2.5 group-data-[size=sm]/code-block:py-2.5 group-data-[size=sm]/code-block:group-has-data-[slot=code-block-header]/code-block:pt-1.5 data-wrap:scroll-fade-y data-wrap:wrap-break-word data-wrap:whitespace-pre-wrap",        className      )}      {...props}    >      <code        data-slot="code-block-code"        className="grid min-h-lh min-w-max content-start in-data-numbered:grid-cols-[auto_1fr] in-data-wrap:min-w-0"      >        {lineNumbers && typeof children === "string"          ? children              .replace(/\n$/, "")              .split("\n")              .map((line, index) => (                <CodeBlockLine key={index}>{line}</CodeBlockLine>              ))          : children}      </code>    </pre>  )}const codeBlockLineVariants = cva(  "relative -mx-3 block min-h-lh px-3 group-data-[size=sm]/code-block:-mx-2.5 group-data-[size=sm]/code-block:px-2.5 before:min-w-6 before:text-end before:text-muted-foreground after:absolute after:start-1 after:top-0 after:text-muted-foreground after:select-none in-data-numbered:col-span-2 in-data-numbered:grid in-data-numbered:grid-cols-subgrid in-data-numbered:gap-x-3 in-data-numbered:[counter-increment:line] in-data-numbered:before:content-[counter(line)] in-data-numbered:before:select-none in-data-numbered:supports-[content:counter(line)_/_'']:before:content-[counter(line)_/_'']",  {    variants: {      variant: {        default: "",        added: "bg-muted after:content-['+']",        removed: "bg-destructive/10 after:text-destructive after:content-['โˆ’']",        highlighted: "bg-muted",      },    },    defaultVariants: {      variant: "default",    },  })function CodeBlockLine({  className,  variant = "default",  children,  ...props}: React.ComponentProps<"span"> & VariantProps<typeof codeBlockLineVariants>) {  // An empty line holds a newline so copying a selection keeps blank lines.  const empty =    children === undefined ||    children === null ||    children === "" ||    (Array.isArray(children) && children.length === 0)  return (    <span      data-slot="code-block-line"      data-variant={variant}      className={cn(codeBlockLineVariants({ variant }), className)}      {...props}    >      <span data-slot="code-block-line-content" className="min-w-0">        {empty ? "\n" : children}      </span>    </span>  )}function CodeBlockFooter({ className, ...props }: React.ComponentProps<"div">) {  return (    <div      data-slot="code-block-footer"      className={cn(        "flex min-w-0 shrink-0 items-center gap-2 px-3 pb-3 text-muted-foreground group-data-[size=sm]/code-block:px-2.5 group-data-[size=sm]/code-block:pb-2.5",        className      )}      {...props}    />  )}export {  CodeBlock,  CodeBlockHeader,  CodeBlockTitle,  CodeBlockActions,  CodeBlockCopyButton,  CodeBlockContent,  CodeBlockLine,  CodeBlockFooter,}
  3. Make sure the foundation is in place

    Components read their colors, radius and surfaces from the tokens in app/globals.css. See Theming.

Usage#

import {  CodeBlock,  CodeBlockHeader,  CodeBlockTitle,  CodeBlockActions,  CodeBlockCopyButton,  CodeBlockContent,  CodeBlockLine,  CodeBlockFooter,} from "@/components/ui/code-block"

Examples#

Actions

Terminal
$ pnpm lint
apps/web/components/message-list.tsx
  18:22  error  'messages' is possibly 'undefined'. Add a guard before calling map  @typescript-eslint/no-unsafe-call
  42:7   warning  React Hook React.useEffect has a missing dependency: 'scrollToBottom'  react-hooks/exhaustive-deps

โœ– 2 problems (1 error, 1 warning)

Ansi

Terminal
$ pnpm testโœ“ src/format.test.ts (4 tests)โœ“ src/parse.test.ts (7 tests)โœ— src/stream.test.ts (3 tests | 1 failed)
AssertionError: expected 'hello' to be 'hello world'
Tests  1 failed | 13 passed (14)

Basic

$ 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

Chat

How do I stream a response?

Create a route handler that returns the stream:

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-4.5",    messages,  })
  return result.toUIMessageStreamResponse()}

The client reads it with useChat.

Destructive

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)

Diff

components/greeting.tsx
export function Greeting({ name }: { name: string }) {  return <p>Hello, {name}</p>  return <p className="text-sm text-muted-foreground">    Hello, {name}!  </p>}

Floating Actions

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

Header

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

Height

Logs
[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 45ms

Highlighted Lines

export default {  experimental: {    reactCompiler: true,    viewTransition: true,  },  images: {    remotePatterns: [{ hostname: "avatars.githubusercontent.com" }],  },}

Highlighting

getWeather output
{  "location": "San Francisco",  "temperature": 18,  "unit": "celsius",  "conditions": ["fog", "wind"]}

Languages

app/globals.css
@import "tailwindcss";
:root {  --radius: 0.625rem;}
main.py
def greet(name: str) -> str:    return f"Hello, {name}!"
notes.txt
A language that is not in the list renders as plain text.

Line Numbers

{  "name": "chat",  "private": true,  "scripts": {    "dev": "next dev",    "build": "next build"  }}

Show More

components/chat.tsx
"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>  )}

Show More Scroll

components/chat.tsx
"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>  )}

Size

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

Start Line

app/dashboard/page.tsx
  if (!session) {    redirect("/login")  }
  const projects = await db.project.findMany({    where: { ownerId: session.user.id },    orderBy: { updatedAt: "desc" },  })
  return <ProjectList projects={projects} />}

Streaming

hooks/use-debounce.ts
export function useDebounce<T>(value: T, delay = 300) {  const [debounced, setDebounced] = React.useState(value)
  React.useEffect(() => {    const timeout = setTimeout(() => setDebounced(value), delay)    return () => clearTimeout(timeout)  }, [value, delay])
  return debounced}
pnpm dlx maya@latest add code-block
components/search.tsx
export function Search() {  const [query, setQuery] = React.useState("")  const results = useSearch(query)  const debounced = useDebounce(query, 300)  const results = useSearch(debounced)
  return <SearchResults results={results} />}
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-4.5",    messages,  })
  return result.toUIMessageStreamResponse()}
.env.local
AI_GATEWAY_API_KEY=your-api-key

Tabs

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-4.5", messages })
  return result.toUIMessageStreamResponse()}

Trace

$ node --version
v24.18.0
$ pnpm dev
Error: listen EADDRINUSE: address already in use :::3000
    at Server.setupListenHandle [as _listen2] (node:net:1908:16)

Typeset

Stream a response

Create a route handler that calls streamText and returns the stream to the client.

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-4.5", messages })
  return result.toUIMessageStreamResponse()}

The useChat hook reads the stream on the client and renders the answer while it is written.

Wrap

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.