# Code Block

A surface for code, terminal output and diffs, with copy, line numbers, line states and streaming syntax highlighting.

Code Block is an open-source component from suiss/ui for React, React Native, SwiftUI and Jetpack Compose. The @esuiss/ui CLI copies its source into your project, so you own the code.

Source: https://suiss.app/ui/docs/components/code-block

**React**

**Example**

```tsx
"use client"

import {
  DownloadSimpleIcon,
  FileCodeIcon,
  TextIndentIcon,
} from "@phosphor-icons/react"
import * as React from "react"
import { Button } from "@/components/ui/button"
import {
  CodeBlock,
  CodeBlockActions,
  CodeBlockContent,
  CodeBlockCopyButton,
  CodeBlockHeader,
  CodeBlockLine,
  CodeBlockTitle,
} from "@/components/ui/code-block"
import { useHighlightedCode } from "@/hooks/use-highlighted-code"

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 default 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)}
            >
              <TextIndentIcon />
            </Button>
            <Button
              variant="ghost"
              size="icon-xs"
              aria-label="Download"
              onClick={() => download(code, "route.ts")}
            >
              <DownloadSimpleIcon />
            </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>
  )
}
```

`CodeBlock` shows code and command output in a rounded surface. Put the code in `CodeBlockContent` and, optionally, a `CodeBlockHeader` with a title and actions above it and a `CodeBlockFooter` below it.

Pass a string to `CodeBlockContent` for plain output, or a list of `CodeBlockLine` when lines need a state or highlighted tokens. `useHighlightedCode` highlights with [Shiki](https://shiki.style) and keeps working while the code streams in.

## Installation

### CLI

```bash title="Terminal"
npx shadcn@latest add @esuiss/code-block
```

Works in any project with a `components.json`. See [Installation](https://suiss.app/ui/docs/installation#add-components) for the one-line registry setup. Or use the [suiss/ui CLI](https://suiss.app/ui/docs/cli):

```bash title="Terminal"
npx @esuiss/ui@latest add code-block
```

### Manual

1. Install the dependencies, add the [`cn` helper](https://suiss.app/ui/docs/installation#the-cn-helper) and the [Button](https://suiss.app/ui/docs/components/button) component:

```bash title="Terminal"
npm install class-variance-authority shiki
```

2. Copy the source into `components/ui/code-block.tsx`:

```tsx title="components/ui/code-block.tsx"
"use client"

import { CheckIcon, CopyIcon } from "@phosphor-icons/react"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react"

import { Button } from "@/components/ui/button"
import { cn } from "@/lib/utils"

const TRAILING_NEWLINE = /\n$/

const codeBlockVariants = cva(
  "rounded-2xl text-sm data-[size=sm]:text-xs has-[>[data-slot=code-block-content]:focus-visible]:ring-3 has-[>[data-slot=code-block-content]:focus-visible]:ring-ring/50 data-[size=sm]:rounded-xl group/code-block relative flex w-full min-w-0 flex-col overflow-hidden",
  {
    variants: {
      variant: {
        default: "bg-muted text-foreground ring-1 ring-foreground/5 dark:ring-foreground/10",
        destructive: "bg-destructive/5 text-destructive ring-1 ring-destructive/15 dark:bg-destructive/10 dark:ring-destructive/20",
      },
    },
    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(
        "gap-2 px-4 pt-4 text-muted-foreground group-data-[variant=destructive]/code-block:text-destructive group-data-[size=sm]/code-block:px-3 group-data-[size=sm]/code-block:pt-3 flex min-w-0 shrink-0 items-center *: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(
        "font-sans text-sm font-medium text-foreground group-data-[variant=destructive]/code-block:text-destructive group-data-[size=sm]/code-block:text-xs min-w-0 truncate [&_svg]:pointer-events-none [&_svg]:inline-block [&_svg]:align-middle *:[svg]:me-1.5 [&_svg:not([class*='size-'])]:size-4 group-data-[size=sm]/code-block:[&_svg:not([class*='size-'])]:size-3.5",
        className
      )}
      {...props}
    />
  )
}

function CodeBlockActions({
  className,
  ...props
}: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="code-block-actions"
      className={cn(
        "ms-auto -my-1 -me-1.5 gap-1 [[data-slot=code-block]>&]:m-0 [[data-slot=code-block]>&]:top-3 [[data-slot=code-block]>&]:end-3 group-data-[size=sm]/code-block:[[data-slot=code-block]>&]:top-2 group-data-[size=sm]/code-block:[[data-slot=code-block]>&]:end-2 flex shrink-0 items-center [[data-slot=code-block]>&]:absolute [[data-slot=code-block]>&]:z-10 [[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 [[data-slot=code-block]>&]:pointer-coarse:opacity-100 *:data-[slot=button]:hover:bg-foreground/5 dark:*:data-[slot=button]:hover:bg-foreground/10 [[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("hover:bg-foreground/5 dark:hover:bg-foreground/10", className)}
        onClick={(event) => {
          onClick?.(event)
          navigator.clipboard
            .writeText(value)
            .then(() => setCopied(true))
            .catch(() => undefined)
        }}
        {...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"
      // biome-ignore lint/a11y/noNoninteractiveTabindex: scrollable code must be reachable by keyboard
      tabIndex={0}
      style={{ counterReset: `line ${startLine - 1}`, ...style }}
      className={cn(
        "px-4 py-4 leading-relaxed group-has-data-[slot=code-block-header]/code-block:pt-2 group-has-data-[slot=code-block-footer]/code-block:pb-2 group-data-[size=sm]/code-block:px-3 group-data-[size=sm]/code-block:py-3 group-data-[size=sm]/code-block:group-has-data-[slot=code-block-header]/code-block:pt-1.5 min-h-0 min-w-0 scroll-fade-x scrollbar-thin overflow-auto text-start font-mono outline-none 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(TRAILING_NEWLINE, "")
              .split("\n")
              .map((line, index) => (
                <CodeBlockLine key={index}>{line}</CodeBlockLine>
              ))
          : children}
      </code>
    </pre>
  )
}

const codeBlockLineVariants = cva(
  "in-data-numbered:before:content-[counter(line)] in-data-numbered:supports-[content:counter(line)_/_'']:before:content-[counter(line)_/_''] -mx-4 px-4 in-data-numbered:gap-x-3 before:min-w-6 before:text-end before:text-muted-foreground after:start-1 after:text-muted-foreground group-data-[size=sm]/code-block:-mx-3 group-data-[size=sm]/code-block:px-3 relative block min-h-lh after:absolute after:top-0 after:select-none in-data-numbered:col-span-2 in-data-numbered:grid in-data-numbered:grid-cols-subgrid in-data-numbered:[counter-increment:line] in-data-numbered:before:select-none",
  {
    variants: {
      variant: {
        default: "",
        added: "bg-foreground/5 after:content-['+']",
        removed: "bg-destructive/10 after:text-destructive after:content-['−']",
        highlighted: "bg-foreground/5",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  }
)

function CodeBlockLine({
  className,
  variant = "default",
  children,
  ...props
}: React.ComponentProps<"span"> & VariantProps<typeof codeBlockLineVariants>) {
  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(
        "gap-2 px-4 pb-4 text-muted-foreground group-data-[size=sm]/code-block:px-3 group-data-[size=sm]/code-block:pb-3 flex min-w-0 shrink-0 items-center",
        className
      )}
      {...props}
    />
  )
}

export {
  CodeBlock,
  CodeBlockActions,
  CodeBlockContent,
  CodeBlockCopyButton,
  CodeBlockFooter,
  CodeBlockHeader,
  CodeBlockLine,
  CodeBlockTitle,
}
```

3. For syntax highlighting, copy the highlighter into `lib/highlighter.tsx`:

- `lib/highlighter.tsx`: [source](https://github.com/e-suiss/ui/blob/main/packages/web/lib/highlighter.tsx)

4. Copy the hook into `hooks/use-highlighted-code.ts`:

- `hooks/use-highlighted-code.ts`: [source](https://github.com/e-suiss/ui/blob/main/packages/web/hooks/use-highlighted-code.ts)

5. Update the import paths to match your project.

## Usage

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

```tsx
<CodeBlock>
  <CodeBlockHeader>
    <CodeBlockTitle>Terminal</CodeBlockTitle>
    <CodeBlockActions>
      <CodeBlockCopyButton value={output} />
    </CodeBlockActions>
  </CodeBlockHeader>
  <CodeBlockContent>{output}</CodeBlockContent>
</CodeBlock>
```

A string child renders as a single text node, so streamed output stays cheap. Line numbers and diff markers are left out of a selection, and the content always reads left to right, while the header follows the page direction.

## Examples

### Basic

A string passed straight to `CodeBlockContent`.

**Example**

```tsx
import { 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 default function CodeBlockBasic() {
  return (
    <div className="w-full max-w-md">
      <CodeBlock>
        <CodeBlockContent>{output}</CodeBlockContent>
      </CodeBlock>
    </div>
  )
}
```

### Header

A `CodeBlockHeader` with a title and a copy button. Long titles truncate.

**Example**

```tsx
import { TerminalWindowIcon } from "@phosphor-icons/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: +14
Progress: resolved 412, reused 398, downloaded 14, added 14, done
Done in 2.8s using pnpm v10.18.0`

export default function CodeBlockHeaderExample() {
  return (
    <div className="w-full max-w-md">
      <CodeBlock>
        <CodeBlockHeader>
          <CodeBlockTitle>
            <TerminalWindowIcon />
            Terminal
          </CodeBlockTitle>
          <CodeBlockActions>
            <CodeBlockCopyButton value={command} />
          </CodeBlockActions>
        </CodeBlockHeader>
        <CodeBlockContent>{output}</CodeBlockContent>
      </CodeBlock>
    </div>
  )
}
```

### Actions

Extra buttons next to the copy button. The wrap toggle keeps its own state and passes it to `wrap`.

**Example**

```tsx
"use client"

import {
  DownloadSimpleIcon,
  TerminalWindowIcon,
  TextIndentIcon,
} from "@phosphor-icons/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 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)`

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 default function CodeBlockActionsExample() {
  const [wrap, setWrap] = React.useState(true)

  return (
    <div className="w-full max-w-md">
      <CodeBlock>
        <CodeBlockHeader>
          <CodeBlockTitle>
            <TerminalWindowIcon />
            Terminal
          </CodeBlockTitle>
          <CodeBlockActions>
            <Button
              variant="ghost"
              size="icon-xs"
              aria-label="Wrap lines"
              aria-pressed={wrap}
              className="aria-pressed:text-foreground"
              onClick={() => setWrap(!wrap)}
            >
              <TextIndentIcon />
            </Button>
            <Button
              variant="ghost"
              size="icon-xs"
              aria-label="Download"
              onClick={() => download(output, "lint.log")}
            >
              <DownloadSimpleIcon />
            </Button>
            <CodeBlockCopyButton value={output} />
          </CodeBlockActions>
        </CodeBlockHeader>
        <CodeBlockContent wrap={wrap}>{output}</CodeBlockContent>
      </CodeBlock>
    </div>
  )
}
```

### Floating actions

Without a header, place `CodeBlockActions` directly in `CodeBlock`. The actions float at the top end and appear on hover or focus, and always on touch screens.

**Example**

```tsx
import {
  CodeBlock,
  CodeBlockActions,
  CodeBlockContent,
  CodeBlockCopyButton,
} from "@/components/ui/code-block"

const output = `$ 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`

export default function CodeBlockFloatingActions() {
  return (
    <div className="w-full max-w-md">
      <CodeBlock>
        <CodeBlockActions>
          <CodeBlockCopyButton value={output} />
        </CodeBlockActions>
        <CodeBlockContent>{output}</CodeBlockContent>
      </CodeBlock>
    </div>
  )
}
```

### Line numbers

Set `lineNumbers` on `CodeBlockContent`. A string child is split into lines.

**Example**

```tsx
import { CodeBlock, CodeBlockContent } from "@/components/ui/code-block"

const code = `{
  "name": "chat",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build"
  }
}`

export default function CodeBlockLineNumbers() {
  return (
    <div className="w-full max-w-md">
      <CodeBlock>
        <CodeBlockContent lineNumbers>{code}</CodeBlockContent>
      </CodeBlock>
    </div>
  )
}
```

### Start line

Set `startLine` to number a snippet from the middle of a file. The gutter widens to fit the largest number.

**Example**

```tsx
"use client"

import {
  CodeBlock,
  CodeBlockContent,
  CodeBlockHeader,
  CodeBlockLine,
  CodeBlockTitle,
} from "@/components/ui/code-block"
import { useHighlightedCode } from "@/hooks/use-highlighted-code"

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 default 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>
  )
}
```

### Wrap

Lines scroll by default. Set `wrap` for tool output and error messages.

**Example**

```tsx
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 default 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>
  )
}
```

### Highlighted lines

Render each line as a `CodeBlockLine` and set `variant="highlighted"` on the ones to call out.

**Example**

```tsx
import {
  CodeBlock,
  CodeBlockContent,
  CodeBlockLine,
} from "@/components/ui/code-block"

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

const highlighted = [2, 3]

export default function CodeBlockHighlightedLines() {
  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>
  )
}
```

### Diff

`variant="added"` and `variant="removed"` tint a line and add a `+` or `−` marker.

**Example**

```tsx
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"
  }

  if (line.startsWith("-")) {
    return "removed"
  }

  return "default"
}

export default 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>
  )
}
```

### Height

A `max-h-*` class on `CodeBlockContent` caps the block. The content scrolls inside and its edges fade with [`scroll-fade`](https://suiss.app/ui/docs/utils/scroll-fade).

**Example**

```tsx
import {
  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 default 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 scroll-fade-y">
          {logs}
        </CodeBlockContent>
      </CodeBlock>
    </div>
  )
}
```

### Show more

Cap the content while collapsed and toggle it from a ghost button in `CodeBlockFooter`.

**Example**

```tsx
"use client"

import { CaretDownIcon } from "@phosphor-icons/react"
import * as React from "react"
import { Button } from "@/components/ui/button"
import {
  CodeBlock,
  CodeBlockActions,
  CodeBlockContent,
  CodeBlockCopyButton,
  CodeBlockFooter,
  CodeBlockHeader,
  CodeBlockLine,
  CodeBlockTitle,
} from "@/components/ui/code-block"
import { useHighlightedCode } from "@/hooks/use-highlighted-code"

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 default 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 scroll-fade-y 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)}
          >
            <CaretDownIcon
              className="transition-transform in-aria-expanded:rotate-180"
            />
            {expanded ? "Show less" : "Show more"}
          </Button>
        </CodeBlockFooter>
      </CodeBlock>
    </div>
  )
}
```

### Show more with max height

Raise the cap instead of removing it, so a long expanded block still scrolls inside the page.

**Example**

```tsx
"use client"

import { CaretDownIcon } from "@phosphor-icons/react"
import * as React from "react"
import { Button } from "@/components/ui/button"
import {
  CodeBlock,
  CodeBlockActions,
  CodeBlockContent,
  CodeBlockCopyButton,
  CodeBlockFooter,
  CodeBlockHeader,
  CodeBlockLine,
  CodeBlockTitle,
} from "@/components/ui/code-block"
import { useHighlightedCode } from "@/hooks/use-highlighted-code"

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 default function CodeBlockShowMoreScroll() {
  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
              ? "max-h-96 scroll-fade-y"
              : "max-h-64 scroll-fade-y 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)}
          >
            <CaretDownIcon
              className="transition-transform in-aria-expanded:rotate-180"
            />
            {expanded ? "Show less" : "Show more"}
          </Button>
        </CodeBlockFooter>
      </CodeBlock>
    </div>
  )
}
```

### Size

`size="sm"` tightens the padding and uses `text-xs`, for tool and trace output.

**Example**

```tsx
import { TerminalWindowIcon } from "@phosphor-icons/react"
import {
  CodeBlock,
  CodeBlockActions,
  CodeBlockContent,
  CodeBlockCopyButton,
  CodeBlockHeader,
  CodeBlockTitle,
} from "@/components/ui/code-block"

const output = `$ 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`

export default function CodeBlockSize() {
  return (
    <div className="flex w-full max-w-md flex-col gap-4">
      <CodeBlock>
        <CodeBlockHeader>
          <CodeBlockTitle>
            <TerminalWindowIcon />
            Terminal
          </CodeBlockTitle>
          <CodeBlockActions>
            <CodeBlockCopyButton value={output} />
          </CodeBlockActions>
        </CodeBlockHeader>
        <CodeBlockContent wrap>{output}</CodeBlockContent>
      </CodeBlock>
      <CodeBlock size="sm">
        <CodeBlockHeader>
          <CodeBlockTitle>
            <TerminalWindowIcon />
            Terminal
          </CodeBlockTitle>
          <CodeBlockActions>
            <CodeBlockCopyButton value={output} />
          </CodeBlockActions>
        </CodeBlockHeader>
        <CodeBlockContent wrap>{output}</CodeBlockContent>
      </CodeBlock>
    </div>
  )
}
```

### Destructive

`variant="destructive"` for errors and failed output.

**Example**

```tsx
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 default 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>
  )
}
```

### Trace

A small, wrapping `CodeBlock` inside a collapsible trace step for terminal output.

**Example**

```tsx
import { TerminalWindowIcon } from "@phosphor-icons/react"
import { CodeBlock, CodeBlockContent } from "@/components/ui/code-block"
import {
  Trace,
  TraceCollapsible,
  TraceCollapsibleContent,
  TraceCollapsibleTrigger,
  TraceIcon,
  TraceLine,
  TraceTitle,
} from "@/components/ui/trace"

const output = `$ node --version
v24.18.0`

const error = `$ pnpm dev
Error: listen EADDRINUSE: address already in use :::3000
    at Server.setupListenHandle [as _listen2] (node:net:1908:16)`

export default function CodeBlockTrace() {
  return (
    <div className="w-full max-w-md">
      <Trace>
        <TraceCollapsible defaultOpen>
          <TraceCollapsibleTrigger>
            <TraceLine>
              <TraceIcon>
                <TerminalWindowIcon />
              </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>
                <TerminalWindowIcon />
              </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>
  )
}
```

### Tabs

Several files in [Tabs](https://suiss.app/ui/docs/components/tabs), one `CodeBlock` per panel.

**Example**

```tsx
"use client"

import {
  CodeBlock,
  CodeBlockActions,
  CodeBlockContent,
  CodeBlockCopyButton,
  CodeBlockHeader,
  CodeBlockLine,
  CodeBlockTitle,
} from "@/components/ui/code-block"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { useHighlightedCode } from "@/hooks/use-highlighted-code"

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 default 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>
  )
}
```

### Message

A `CodeBlock` between paragraphs of a [Message](https://suiss.app/ui/docs/components/message).

**Example**

```tsx
"use client"

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"
import { useHighlightedCode } from "@/hooks/use-highlighted-code"

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 default function CodeBlockChat() {
  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 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>
  )
}
```

### Syntax highlighting

`useHighlightedCode` returns one React node per line, ready for `CodeBlockLine`.

**Example**

```tsx
"use client"

import {
  CodeBlock,
  CodeBlockContent,
  CodeBlockHeader,
  CodeBlockLine,
  CodeBlockTitle,
} from "@/components/ui/code-block"
import { useHighlightedCode } from "@/hooks/use-highlighted-code"

const code = `{
  "location": "San Francisco",
  "temperature": 18,
  "unit": "celsius",
  "conditions": ["fog", "wind"]
}`

export default 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>
  )
}
```

```tsx
const lines = useHighlightedCode(code, "json")

<CodeBlock>
  <CodeBlockContent>
    {lines.map((line, index) => (
      <CodeBlockLine key={index}>{line}</CodeBlockLine>
    ))}
  </CodeBlockContent>
</CodeBlock>
```

Shiki runs on its JavaScript regex engine, so there is no WASM to load.

### Languages

Each language loads on first use. The list lives in `lib/highlighter.tsx` and 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`. Any other language renders as plain text.

**Example**

```tsx
"use client"

import {
  CodeBlock,
  CodeBlockContent,
  CodeBlockHeader,
  CodeBlockLine,
  CodeBlockTitle,
} from "@/components/ui/code-block"
import { useHighlightedCode } from "@/hooks/use-highlighted-code"

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 default 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>
  )
}
```

### Terminal output

Pass `"ansi"` as the language to turn escape codes into colors. Colors do not carry over from one line to the next.

**Example**

```tsx
"use client"

import { TerminalWindowIcon } from "@phosphor-icons/react"
import {
  CodeBlock,
  CodeBlockActions,
  CodeBlockContent,
  CodeBlockCopyButton,
  CodeBlockHeader,
  CodeBlockLine,
  CodeBlockTitle,
} from "@/components/ui/code-block"
import { useHighlightedCode } from "@/hooks/use-highlighted-code"

const ANSI_CODES = new RegExp(`${String.fromCharCode(27)}\\[[\\d;]*m`, "g")

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 default function CodeBlockAnsi() {
  const lines = useHighlightedCode(output, "ansi")

  return (
    <div className="w-full max-w-md">
      <CodeBlock>
        <CodeBlockHeader>
          <CodeBlockTitle>
            <TerminalWindowIcon />
            Terminal
          </CodeBlockTitle>
          <CodeBlockActions>
            <CodeBlockCopyButton value={output.replace(ANSI_CODES, "")} />
          </CodeBlockActions>
        </CodeBlockHeader>
        <CodeBlockContent wrap>
          {lines.map((line, index) => (
            <CodeBlockLine key={index}>{line}</CodeBlockLine>
          ))}
        </CodeBlockContent>
      </CodeBlock>
    </div>
  )
}
```

### Streaming

While code streams in, the hook tokenizes only the new lines. Unchanged lines keep their nodes, so React skips them.

**Example**

```tsx
"use client"

import { ArrowCounterClockwiseIcon } from "@phosphor-icons/react"
import * as React from "react"
import { Button } from "@/components/ui/button"
import {
  CodeBlock,
  CodeBlockActions,
  CodeBlockContent,
  CodeBlockCopyButton,
  CodeBlockHeader,
  CodeBlockLine,
  CodeBlockTitle,
} from "@/components/ui/code-block"
import { useHighlightedCode } from "@/hooks/use-highlighted-code"

const blocks: {
  title?: string
  language: string
  code: string
  lineNumbers?: boolean
  highlight?: number[]
  diff?: boolean
}[] = [
  {
    title: "hooks/use-debounce.ts",
    language: "ts",
    lineNumbers: true,
    code: `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
}`,
  },
  {
    language: "bash",
    code: "pnpm dlx shadcn@latest add code-block",
  },
  {
    title: "components/search.tsx",
    language: "tsx",
    lineNumbers: true,
    diff: true,
    code: ` 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} />
 }`,
  },
  {
    title: "app/api/chat/route.ts",
    language: "ts",
    lineNumbers: true,
    highlight: [5, 6, 7, 8],
    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()
}`,
  },
  {
    title: ".env.local",
    language: "bash",
    code: "AI_GATEWAY_API_KEY=your-api-key",
  },
]
const offsets = blocks.map((_, index) =>
  blocks.slice(0, index).reduce((total, block) => total + block.code.length, 0)
)
const total = blocks.reduce((sum, block) => sum + block.code.length, 0)

function lineVariant(
  block: (typeof blocks)[number],
  row: string | undefined,
  index: number
) {
  if (block.diff) {
    if (row?.startsWith("+")) return "added"
    if (row?.startsWith("-")) return "removed"
    return "default"
  }
  return block.highlight?.includes(index) ? "highlighted" : "default"
}

function StreamingBlock({
  block,
  code,
}: {
  block: (typeof blocks)[number]
  code: string
}) {
  const rows = code.split("\n")
  const source = block.diff ? rows.map((row) => row.slice(1)).join("\n") : code
  const lines = useHighlightedCode(source, block.language)

  return (
    <CodeBlock>
      {block.title && (
        <CodeBlockHeader>
          <CodeBlockTitle>{block.title}</CodeBlockTitle>
          <CodeBlockActions>
            <CodeBlockCopyButton
              value={source}
              disabled={code.length < block.code.length}
            />
          </CodeBlockActions>
        </CodeBlockHeader>
      )}
      <CodeBlockContent lineNumbers={block.lineNumbers}>
        {lines.map((line, index) => (
          <CodeBlockLine
            key={index}
            variant={lineVariant(block, rows[index], index)}
          >
            {line}
          </CodeBlockLine>
        ))}
      </CodeBlockContent>
    </CodeBlock>
  )
}

export default function CodeBlockStreaming() {
  const [length, setLength] = React.useState(total)

  React.useEffect(() => {
    if (length >= total) {
      return
    }

    const timeout = setTimeout(() => setLength(length + 3), 16)
    return () => clearTimeout(timeout)
  }, [length])

  return (
    <div className="flex w-full max-w-md flex-col gap-4">
      {blocks.map(
        (block, index) =>
          length > (offsets.at(index) ?? 0) && (
            <StreamingBlock
              key={block.code}
              block={block}
              code={block.code.slice(0, length - (offsets.at(index) ?? 0))}
            />
          )
      )}
      <Button
        variant="outline"
        size="sm"
        className="self-start"
        disabled={length < total}
        onClick={() => setLength(0)}
      >
        <ArrowCounterClockwiseIcon />
        Replay
      </Button>
    </div>
  )
}
```

### Server rendering

`highlightCode` is async and runs in a Server Component, which keeps Shiki out of the client bundle.

```tsx
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>
  )
}
```

## API Reference

- `CodeBlock`
- `CodeBlockHeader`
  - `CodeBlockTitle`
  - `CodeBlockActions`
    - `CodeBlockCopyButton`
- `CodeBlockContent`
  - `CodeBlockLine`
- `CodeBlockFooter`

### CodeBlock

The surface that holds the header, the content and the footer.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | `"default" \| "destructive"` | `"default"` | The tone of the block. |
| `size` | `"default" \| "sm"` | `"default"` | `sm` uses tighter padding and `text-xs`, for tool output. |

Also accepts the props of `React.ComponentProps<"div">`.

### CodeBlockHeader

A row above the content for a title and actions.

Also accepts the props of `React.ComponentProps<"div">`.

### CodeBlockTitle

A filename or label with an optional icon. It truncates.

Also accepts the props of `React.ComponentProps<"div">`.

### CodeBlockActions

Buttons at the end of the header. As a direct child of `CodeBlock`, it floats over the content.

Also accepts the props of `React.ComponentProps<"div">`.

### CodeBlockCopyButton

A ghost icon button that copies `value` and announces "Copied" in a polite live region.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `string` | - | Required. The text to copy. |

Also accepts the props of [`Button`](https://suiss.app/ui/docs/components/button).

### CodeBlockContent

The `pre` and `code` pair that scrolls or wraps.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `React.ReactNode` | - | A string, or a list of `CodeBlockLine`. |
| `wrap` | `boolean` | `false` | Wraps long lines instead of scrolling. |
| `lineNumbers` | `boolean` | `false` | Numbers each line. A string child is split into lines. |
| `startLine` | `number` | `1` | The number of the first line when `lineNumbers` is set. |
| `tabIndex` | `number` | `0` | Keeps the content focusable so it scrolls by keyboard. |

Also accepts the props of `React.ComponentProps<"pre">`.

### CodeBlockLine

One line of code.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | `"default" \| "added" \| "removed" \| "highlighted"` | `"default"` | The state of the line. |

Also accepts the props of `React.ComponentProps<"span">`.

### CodeBlockFooter

A row below the content, such as for a Show more button.

Also accepts the props of `React.ComponentProps<"div">`.

### useHighlightedCode

```tsx
const lines = useHighlightedCode(code, language)
```

Returns one React node per line. Until the first pass finishes, and for a language outside the list, it returns plain lines.

| Argument | Type | Description |
| --- | --- | --- |
| `code` | `string` | The code to highlight. A trailing newline is ignored. |
| `language` | `string` | A language or alias from `lib/highlighter.tsx`, or `"ansi"`. |

## Accessibility

- `CodeBlockContent` renders a `pre` with a `code` element and always sets `dir="ltr"`, so code reads left to right on a right-to-left page.
- The content has `tabIndex={0}` so keyboard users can focus and scroll it. Pass `tabIndex={-1}` to remove the tab stop.
- Floating actions appear when focus is inside the block, not only on hover.
- Line numbers are hidden from screen readers.
- Added and removed lines carry a `+` or `−` marker as well as a tint, so the change does not rely on color.
- `CodeBlockCopyButton` is labelled "Copy" and announces "Copied" after a successful copy.
- Give a wrap toggle `aria-pressed`, and a Show more button `aria-expanded` with `aria-controls` pointing at the content `id`.

## Notes

- Styling hooks: `data-slot="code-block"`, `code-block-header`, `code-block-title`, `code-block-actions`, `code-block-copy-button`, `code-block-content`, `code-block-code`, `code-block-line` and `code-block-footer`. The root exposes `data-variant` and `data-size`, and each line exposes `data-variant`.

**React Native, SwiftUI, Jetpack Compose**

```tsx
import { FileTsIcon } from "phosphor-react-native"
import { CodeBlock, CodeBlockActions, CodeBlockContent, CodeBlockCopyButton, CodeBlockHeader, CodeBlockTitle } from "@/components/ui/code-block"
import { Icon } from "@/components/ui/icon"
import { Text } from "@/components/ui/text"

const greeting = `export function Greeting({ name }: { name: string }) {
  // Say hello to the person signing in
  return <Text>Hello, {name}!</Text>
}`

export function CodeBlockExample() {
  return (
    <CodeBlock>
      <CodeBlockHeader>
        <Icon as={FileTsIcon} />
        <CodeBlockTitle>components/greeting.tsx</CodeBlockTitle>
        <CodeBlockActions>
          <CodeBlockCopyButton value={greeting} />
        </CodeBlockActions>
      </CodeBlockHeader>
      <CodeBlockContent language="tsx">{greeting}</CodeBlockContent>
    </CodeBlock>
  )
}
```

```swift
import EsuissUI
import PhosphorSwift
import SwiftUI

struct CodeBlockExample: View {
  var body: some View {
    CodeBlock {
      CodeBlockHeader {
        Ph.fileTs.regular.frame(width: 16, height: 16)
        CodeBlockTitle(verbatim: "components/greeting.tsx")
        CodeBlockActions { CodeBlockCopyButton(value: greeting) }
      }
      CodeBlockContent(greeting, language: "tsx")
    }
  }
}

let greeting = """
```

```kotlin
import androidx.compose.runtime.Composable
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.FileTs
import com.esuiss.ui.components.CodeBlock
import com.esuiss.ui.components.CodeBlockActions
import com.esuiss.ui.components.CodeBlockContent
import com.esuiss.ui.components.CodeBlockCopyButton
import com.esuiss.ui.components.CodeBlockHeader
import com.esuiss.ui.components.CodeBlockTitle
import com.esuiss.ui.components.Icon

@Composable
fun CodeBlockDefault() {
    CodeBlock {
        CodeBlockHeader {
            Icon(PhosphorIcons.Regular.FileTs, contentDescription = null)
            CodeBlockTitle("components/greeting.tsx")
            CodeBlockActions { CodeBlockCopyButton(GREETING) }
        }
        CodeBlockContent(GREETING, language = "tsx")
    }
}
```

A `CodeBlock` shows code in a rounded, muted surface. Put a `CodeBlockTitle` and `CodeBlockActions` with a `CodeBlockCopyButton` in a `CodeBlockHeader`, the code in `CodeBlockContent`, and secondary text in a `CodeBlockFooter`.

`CodeBlockContent` takes a string or `CodeBlockLine` children. Long lines scroll sideways unless you set `wrap`. Pass `language` to color keywords, strings, numbers and comments with a small built-in highlighter that uses the theme colors, so it follows light and dark mode and every style.

## Installation

**React Native**

```bash title="Terminal"
npx shadcn@latest add @esuiss-rn/code-block
```

See [Installation](https://suiss.app/ui/docs/installation) for the `components.json` setup. Or use the [suiss/ui CLI](https://suiss.app/ui/docs/cli):

```bash title="Terminal"
npx @esuiss/ui@latest add code-block
```

**SwiftUI**

```bash title="Terminal"
npx @esuiss/ui@latest add code-block --platform swift
```

**Jetpack Compose**

```bash title="Terminal"
npx @esuiss/ui@latest add code-block --platform kotlin
```

The CLI also adds what it builds on: [Icon](https://suiss.app/ui/docs/components/icon), [Text](https://suiss.app/ui/docs/components/text).

### Manual

**React Native**

**Uniwind**

1. Install the dependencies:

```bash title="Terminal"
npx expo install class-variance-authority cn expo-clipboard phosphor-react-native react-native-svg
```

2. Add the pieces it builds on: [Icon](https://suiss.app/ui/docs/components/icon), [Text](https://suiss.app/ui/docs/components/text).

3. Copy the source into `components/ui/code-block.tsx`:

```tsx title="uniwind/components/ui/code-block.tsx"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"
import { setStringAsync } from "expo-clipboard"
import { CheckIcon, CopyIcon } from "phosphor-react-native"
import * as React from "react"
import { Pressable, ScrollView, View, type ViewProps } from "react-native"
import { Icon, IconClassContext } from "@/components/ui/icon"
import { Text, TextClassContext } from "@/components/ui/text"

type CodeBlockSize = "default" | "sm"
type CodeBlockVariant = "default" | "destructive"
type TokenKind = "plain" | "keyword" | "string" | "number" | "comment"

const KEYWORDS: Record<string, string[]> = {
  script: [
    "as",
    "async",
    "await",
    "break",
    "case",
    "catch",
    "class",
    "const",
    "continue",
    "default",
    "delete",
    "do",
    "else",
    "export",
    "extends",
    "false",
    "finally",
    "for",
    "from",
    "function",
    "if",
    "import",
    "in",
    "instanceof",
    "interface",
    "let",
    "new",
    "null",
    "of",
    "return",
    "satisfies",
    "static",
    "super",
    "switch",
    "this",
    "throw",
    "true",
    "try",
    "type",
    "typeof",
    "undefined",
    "var",
    "void",
    "while",
    "yield",
  ],
  json: ["true", "false", "null"],
  shell: [
    "cd",
    "do",
    "done",
    "echo",
    "elif",
    "else",
    "esac",
    "export",
    "fi",
    "for",
    "if",
    "in",
    "npm",
    "npx",
    "pnpm",
    "then",
    "while",
  ],
  python: [
    "and",
    "as",
    "class",
    "def",
    "elif",
    "else",
    "False",
    "for",
    "from",
    "if",
    "import",
    "in",
    "is",
    "None",
    "not",
    "or",
    "pass",
    "return",
    "True",
    "while",
    "with",
    "yield",
  ],
  swift: [
    "case",
    "class",
    "else",
    "enum",
    "extension",
    "false",
    "for",
    "func",
    "guard",
    "if",
    "import",
    "in",
    "let",
    "nil",
    "private",
    "protocol",
    "public",
    "return",
    "self",
    "static",
    "struct",
    "switch",
    "true",
    "var",
    "where",
    "while",
  ],
  kotlin: [
    "class",
    "data",
    "else",
    "enum",
    "false",
    "for",
    "fun",
    "if",
    "import",
    "in",
    "is",
    "null",
    "object",
    "package",
    "private",
    "return",
    "true",
    "val",
    "var",
    "when",
    "while",
  ],
}

const LANGUAGES: Record<string, keyof typeof KEYWORDS> = {
  bash: "shell",
  js: "script",
  javascript: "script",
  json: "json",
  jsx: "script",
  kotlin: "kotlin",
  kt: "kotlin",
  py: "python",
  python: "python",
  sh: "shell",
  shell: "shell",
  swift: "swift",
  ts: "script",
  tsx: "script",
  typescript: "script",
  yaml: "shell",
  yml: "shell",
}

const HASH_COMMENTS = new Set(["shell", "python"])
const TOKEN =
  /("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`)|(\b\d+(?:\.\d+)?\b)|([A-Za-z_$][\w$]*)/g
const TRAILING_NEWLINE = /\n$/

function tokenize(line: string, language?: string) {
  const group = language ? LANGUAGES[language.toLowerCase()] : undefined
  if (!group) return [{ kind: "plain" as TokenKind, text: line }]
  const keywords = new Set(KEYWORDS[group])
  const marker = HASH_COMMENTS.has(group) ? "#" : "//"
  const comment = line.indexOf(marker)
  const code = comment >= 0 ? line.slice(0, comment) : line
  const tokens: { kind: TokenKind; text: string }[] = []
  let last = 0
  for (const match of code.matchAll(TOKEN)) {
    const [text, string, number, word] = match
    const index = match.index ?? 0
    if (index > last)
      tokens.push({ kind: "plain", text: code.slice(last, index) })
    let kind: TokenKind = "plain"
    if (string) kind = "string"
    else if (number) kind = "number"
    else if (word && keywords.has(word)) kind = "keyword"
    tokens.push({ kind, text })
    last = index + text.length
  }
  if (last < code.length) tokens.push({ kind: "plain", text: code.slice(last) })
  if (comment >= 0) tokens.push({ kind: "comment", text: line.slice(comment) })
  return tokens
}

const TOKEN_CLASSES: Record<TokenKind, string | undefined> = {
  plain: undefined,
  keyword: "text-chart-3",
  string: "text-chart-2",
  number: "text-chart-4",
  comment: "text-muted-foreground",
}

const CodeBlockContext = React.createContext<{
  size: CodeBlockSize
  variant: CodeBlockVariant
}>({ size: "default", variant: "default" })

const CodeBlockLineContext = React.createContext<{
  number?: number
  numbered: boolean
}>({ numbered: false })

const codeBlockVariants = cva("w-full min-w-0 overflow-hidden", {
  variants: {
    variant: {
      default: "border border-foreground/5 bg-muted",
      destructive: "border border-destructive/15 bg-destructive/5",
    },
    size: {
      default: "rounded-2xl",
      sm: "rounded-xl",
    },
  },
  defaultVariants: {
    variant: "default",
    size: "default",
  },
})

function CodeBlock({
  className,
  variant = "default",
  size = "default",
  ...props
}: ViewProps & VariantProps<typeof codeBlockVariants>) {
  const resolvedVariant = variant ?? "default"
  const resolvedSize = size ?? "default"
  return (
    <CodeBlockContext.Provider
      value={{ size: resolvedSize, variant: resolvedVariant }}
    >
      <TextClassContext.Provider
        value={cn(
          "font-mono",
          resolvedSize === "sm" ? "text-xs" : "text-sm",
          resolvedVariant === "destructive"
            ? "text-destructive"
            : "text-foreground"
        )}
      >
        <View
          className={cn(
            codeBlockVariants({ variant: resolvedVariant, size: resolvedSize }),
            className
          )}
          {...props}
        />
      </TextClassContext.Provider>
    </CodeBlockContext.Provider>
  )
}

function CodeBlockHeader({ className, ...props }: ViewProps) {
  const { size, variant } = React.useContext(CodeBlockContext)
  const tone =
    variant === "destructive" ? "text-destructive" : "text-muted-foreground"
  return (
    <IconClassContext.Provider value={cn("size-4", tone)}>
      <View
        className={cn(
          "flex-row items-center gap-2",
          size === "sm" ? "px-3 pt-3" : "px-4 pt-4",
          className
        )}
        {...props}
      />
    </IconClassContext.Provider>
  )
}

function CodeBlockTitle({
  className,
  ...props
}: React.ComponentProps<typeof Text>) {
  const { size, variant } = React.useContext(CodeBlockContext)
  return (
    <Text
      numberOfLines={1}
      className={cn(
        "min-w-0 shrink font-sans font-medium",
        size === "sm" ? "text-xs" : "text-sm",
        variant === "destructive" ? "text-destructive" : "text-foreground",
        className
      )}
      {...props}
    />
  )
}

function CodeBlockActions({ className, ...props }: ViewProps) {
  return (
    <View
      className={cn("ms-auto flex-row items-center gap-1", className)}
      {...props}
    />
  )
}

function CodeBlockCopyButton({
  value,
  className,
  onPress,
  ...props
}: Omit<React.ComponentProps<typeof Pressable>, "children"> & {
  value: string
}) {
  const [copied, setCopied] = React.useState(false)

  React.useEffect(() => {
    if (!copied) return
    const timeout = setTimeout(() => setCopied(false), 2000)
    return () => clearTimeout(timeout)
  }, [copied])

  return (
    <Pressable
      accessibilityRole="button"
      accessibilityLabel={copied ? "Copied" : "Copy"}
      hitSlop={8}
      className={cn(
        "size-6 items-center justify-center rounded-md active:bg-foreground/10",
        className
      )}
      onPress={(event) => {
        onPress?.(event)
        setStringAsync(value)
          .then(() => setCopied(true))
          .catch(() => undefined)
      }}
      {...props}
    >
      <Icon
        as={copied ? CheckIcon : CopyIcon}
        className="size-3.5 text-muted-foreground"
      />
    </Pressable>
  )
}

function CodeBlockContent({
  className,
  children,
  wrap = false,
  lineNumbers = false,
  startLine = 1,
  language,
  ...props
}: ViewProps & {
  wrap?: boolean
  lineNumbers?: boolean
  startLine?: number
  language?: string
}) {
  const { size } = React.useContext(CodeBlockContext)
  const lines =
    typeof children === "string"
      ? children
          .replace(TRAILING_NEWLINE, "")
          .split("\n")
          .map((line, index) => (
            // biome-ignore lint/suspicious/noArrayIndexKey: lines are positional
            <CodeBlockLine key={index}>
              {tokenize(line, language).map((token, position) => (
                <Text
                  // biome-ignore lint/suspicious/noArrayIndexKey: tokens are positional
                  key={position}
                  className={TOKEN_CLASSES[token.kind]}
                >
                  {token.text}
                </Text>
              ))}
            </CodeBlockLine>
          ))
      : React.Children.toArray(children)
  const body = (
    <View className={cn("min-w-full", size === "sm" ? "py-3" : "py-4")}>
      {lines.map((line, index) => (
        <CodeBlockLineContext.Provider
          // biome-ignore lint/suspicious/noArrayIndexKey: lines are positional
          key={index}
          value={{ numbered: lineNumbers, number: startLine + index }}
        >
          {line}
        </CodeBlockLineContext.Provider>
      ))}
    </View>
  )
  return (
    <View className={cn("min-h-0 min-w-0", className)} {...props}>
      {wrap ? (
        body
      ) : (
        <ScrollView horizontal showsHorizontalScrollIndicator={false}>
          {body}
        </ScrollView>
      )}
    </View>
  )
}

const LINE_MARKS: Record<string, string | undefined> = {
  added: "+",
  removed: "−",
}

const codeBlockLineVariants = cva("relative min-w-full flex-row", {
  variants: {
    variant: {
      default: "",
      added: "bg-foreground/5",
      removed: "bg-destructive/10",
      highlighted: "bg-foreground/5",
    },
  },
  defaultVariants: {
    variant: "default",
  },
})

function CodeBlockLine({
  className,
  variant = "default",
  children,
  ...props
}: ViewProps & VariantProps<typeof codeBlockLineVariants>) {
  const { size } = React.useContext(CodeBlockContext)
  const { number, numbered } = React.useContext(CodeBlockLineContext)
  const mark = variant ? LINE_MARKS[variant] : undefined
  return (
    <View
      className={cn(
        codeBlockLineVariants({ variant }),
        size === "sm" ? "px-3" : "px-4",
        className
      )}
      {...props}
    >
      {mark ? (
        <Text
          accessible={false}
          className={cn(
            "absolute start-1",
            variant === "removed" ? "text-destructive" : "text-muted-foreground"
          )}
        >
          {mark}
        </Text>
      ) : null}
      {numbered ? (
        <Text
          accessible={false}
          className="me-3 min-w-6 text-end text-muted-foreground"
        >
          {number}
        </Text>
      ) : null}
      <Text className="min-w-0 shrink">{children ?? " "}</Text>
    </View>
  )
}

function CodeBlockFooter({ className, ...props }: ViewProps) {
  const { size } = React.useContext(CodeBlockContext)
  return (
    <TextClassContext.Provider value="font-sans text-xs text-muted-foreground">
      <View
        className={cn(
          "flex-row items-center gap-2",
          size === "sm" ? "px-3 pb-3" : "px-4 pb-4",
          className
        )}
        {...props}
      />
    </TextClassContext.Provider>
  )
}

export {
  CodeBlock,
  CodeBlockActions,
  CodeBlockContent,
  CodeBlockCopyButton,
  CodeBlockFooter,
  CodeBlockHeader,
  CodeBlockLine,
  CodeBlockTitle,
}
```

4. Update the import paths to match your project.

**NativeWind**

1. Install the dependencies:

```bash title="Terminal"
npx expo install class-variance-authority cn expo-clipboard phosphor-react-native react-native-svg
```

2. Add the pieces it builds on: [Icon](https://suiss.app/ui/docs/components/icon), [Text](https://suiss.app/ui/docs/components/text).

3. Copy the source into `components/ui/code-block.tsx`:

```tsx title="nativewind/components/ui/code-block.tsx"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"
import { setStringAsync } from "expo-clipboard"
import { CheckIcon, CopyIcon } from "phosphor-react-native"
import * as React from "react"
import { Pressable, ScrollView, View, type ViewProps } from "react-native"
import { Icon, IconClassContext } from "@/components/ui/icon"
import { Text, TextClassContext } from "@/components/ui/text"

type CodeBlockSize = "default" | "sm"
type CodeBlockVariant = "default" | "destructive"
type TokenKind = "plain" | "keyword" | "string" | "number" | "comment"

const KEYWORDS: Record<string, string[]> = {
  script: [
    "as",
    "async",
    "await",
    "break",
    "case",
    "catch",
    "class",
    "const",
    "continue",
    "default",
    "delete",
    "do",
    "else",
    "export",
    "extends",
    "false",
    "finally",
    "for",
    "from",
    "function",
    "if",
    "import",
    "in",
    "instanceof",
    "interface",
    "let",
    "new",
    "null",
    "of",
    "return",
    "satisfies",
    "static",
    "super",
    "switch",
    "this",
    "throw",
    "true",
    "try",
    "type",
    "typeof",
    "undefined",
    "var",
    "void",
    "while",
    "yield",
  ],
  json: ["true", "false", "null"],
  shell: [
    "cd",
    "do",
    "done",
    "echo",
    "elif",
    "else",
    "esac",
    "export",
    "fi",
    "for",
    "if",
    "in",
    "npm",
    "npx",
    "pnpm",
    "then",
    "while",
  ],
  python: [
    "and",
    "as",
    "class",
    "def",
    "elif",
    "else",
    "False",
    "for",
    "from",
    "if",
    "import",
    "in",
    "is",
    "None",
    "not",
    "or",
    "pass",
    "return",
    "True",
    "while",
    "with",
    "yield",
  ],
  swift: [
    "case",
    "class",
    "else",
    "enum",
    "extension",
    "false",
    "for",
    "func",
    "guard",
    "if",
    "import",
    "in",
    "let",
    "nil",
    "private",
    "protocol",
    "public",
    "return",
    "self",
    "static",
    "struct",
    "switch",
    "true",
    "var",
    "where",
    "while",
  ],
  kotlin: [
    "class",
    "data",
    "else",
    "enum",
    "false",
    "for",
    "fun",
    "if",
    "import",
    "in",
    "is",
    "null",
    "object",
    "package",
    "private",
    "return",
    "true",
    "val",
    "var",
    "when",
    "while",
  ],
}

const LANGUAGES: Record<string, keyof typeof KEYWORDS> = {
  bash: "shell",
  js: "script",
  javascript: "script",
  json: "json",
  jsx: "script",
  kotlin: "kotlin",
  kt: "kotlin",
  py: "python",
  python: "python",
  sh: "shell",
  shell: "shell",
  swift: "swift",
  ts: "script",
  tsx: "script",
  typescript: "script",
  yaml: "shell",
  yml: "shell",
}

const HASH_COMMENTS = new Set(["shell", "python"])
const TOKEN =
  /("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`)|(\b\d+(?:\.\d+)?\b)|([A-Za-z_$][\w$]*)/g
const TRAILING_NEWLINE = /\n$/

function tokenize(line: string, language?: string) {
  const group = language ? LANGUAGES[language.toLowerCase()] : undefined
  if (!group) return [{ kind: "plain" as TokenKind, text: line }]
  const keywords = new Set(KEYWORDS[group])
  const marker = HASH_COMMENTS.has(group) ? "#" : "//"
  const comment = line.indexOf(marker)
  const code = comment >= 0 ? line.slice(0, comment) : line
  const tokens: { kind: TokenKind; text: string }[] = []
  let last = 0
  for (const match of code.matchAll(TOKEN)) {
    const [text, string, number, word] = match
    const index = match.index ?? 0
    if (index > last)
      tokens.push({ kind: "plain", text: code.slice(last, index) })
    let kind: TokenKind = "plain"
    if (string) kind = "string"
    else if (number) kind = "number"
    else if (word && keywords.has(word)) kind = "keyword"
    tokens.push({ kind, text })
    last = index + text.length
  }
  if (last < code.length) tokens.push({ kind: "plain", text: code.slice(last) })
  if (comment >= 0) tokens.push({ kind: "comment", text: line.slice(comment) })
  return tokens
}

const TOKEN_CLASSES: Record<TokenKind, string | undefined> = {
  plain: undefined,
  keyword: "text-chart-3",
  string: "text-chart-2",
  number: "text-chart-4",
  comment: "text-muted-foreground",
}

const CodeBlockContext = React.createContext<{
  size: CodeBlockSize
  variant: CodeBlockVariant
}>({ size: "default", variant: "default" })

const CodeBlockLineContext = React.createContext<{
  number?: number
  numbered: boolean
}>({ numbered: false })

const codeBlockVariants = cva("w-full min-w-0 overflow-hidden", {
  variants: {
    variant: {
      default: "border border-foreground-5 bg-muted",
      destructive: "border border-destructive-15 bg-destructive-5",
    },
    size: {
      default: "rounded-2xl",
      sm: "rounded-xl",
    },
  },
  defaultVariants: {
    variant: "default",
    size: "default",
  },
})

function CodeBlock({
  className,
  variant = "default",
  size = "default",
  ...props
}: ViewProps & VariantProps<typeof codeBlockVariants>) {
  const resolvedVariant = variant ?? "default"
  const resolvedSize = size ?? "default"
  return (
    <CodeBlockContext.Provider
      value={{ size: resolvedSize, variant: resolvedVariant }}
    >
      <TextClassContext.Provider
        value={cn(
          "font-mono",
          resolvedSize === "sm" ? "text-xs" : "text-sm",
          resolvedVariant === "destructive"
            ? "text-destructive"
            : "text-foreground"
        )}
      >
        <View
          className={cn(
            codeBlockVariants({ variant: resolvedVariant, size: resolvedSize }),
            className
          )}
          {...props}
        />
      </TextClassContext.Provider>
    </CodeBlockContext.Provider>
  )
}

function CodeBlockHeader({ className, ...props }: ViewProps) {
  const { size, variant } = React.useContext(CodeBlockContext)
  const tone =
    variant === "destructive" ? "text-destructive" : "text-muted-foreground"
  return (
    <IconClassContext.Provider value={cn("size-4", tone)}>
      <View
        className={cn(
          "flex-row items-center gap-2",
          size === "sm" ? "px-3 pt-3" : "px-4 pt-4",
          className
        )}
        {...props}
      />
    </IconClassContext.Provider>
  )
}

function CodeBlockTitle({
  className,
  ...props
}: React.ComponentProps<typeof Text>) {
  const { size, variant } = React.useContext(CodeBlockContext)
  return (
    <Text
      numberOfLines={1}
      className={cn(
        "min-w-0 shrink font-sans font-medium",
        size === "sm" ? "text-xs" : "text-sm",
        variant === "destructive" ? "text-destructive" : "text-foreground",
        className
      )}
      {...props}
    />
  )
}

function CodeBlockActions({ className, ...props }: ViewProps) {
  return (
    <View
      className={cn("ms-auto flex-row items-center gap-1", className)}
      {...props}
    />
  )
}

function CodeBlockCopyButton({
  value,
  className,
  onPress,
  ...props
}: Omit<React.ComponentProps<typeof Pressable>, "children"> & {
  value: string
}) {
  const [copied, setCopied] = React.useState(false)

  React.useEffect(() => {
    if (!copied) return
    const timeout = setTimeout(() => setCopied(false), 2000)
    return () => clearTimeout(timeout)
  }, [copied])

  return (
    <Pressable
      accessibilityRole="button"
      accessibilityLabel={copied ? "Copied" : "Copy"}
      hitSlop={8}
      className={cn(
        "size-6 items-center justify-center rounded-md active:bg-foreground-10",
        className
      )}
      onPress={(event) => {
        onPress?.(event)
        setStringAsync(value)
          .then(() => setCopied(true))
          .catch(() => undefined)
      }}
      {...props}
    >
      <Icon
        as={copied ? CheckIcon : CopyIcon}
        className="size-3.5 text-muted-foreground"
      />
    </Pressable>
  )
}

function CodeBlockContent({
  className,
  children,
  wrap = false,
  lineNumbers = false,
  startLine = 1,
  language,
  ...props
}: ViewProps & {
  wrap?: boolean
  lineNumbers?: boolean
  startLine?: number
  language?: string
}) {
  const { size } = React.useContext(CodeBlockContext)
  const lines =
    typeof children === "string"
      ? children
          .replace(TRAILING_NEWLINE, "")
          .split("\n")
          .map((line, index) => (
            // biome-ignore lint/suspicious/noArrayIndexKey: lines are positional
            <CodeBlockLine key={index}>
              {tokenize(line, language).map((token, position) => (
                <Text
                  // biome-ignore lint/suspicious/noArrayIndexKey: tokens are positional
                  key={position}
                  className={TOKEN_CLASSES[token.kind]}
                >
                  {token.text}
                </Text>
              ))}
            </CodeBlockLine>
          ))
      : React.Children.toArray(children)
  const body = (
    <View className={cn("min-w-full", size === "sm" ? "py-3" : "py-4")}>
      {lines.map((line, index) => (
        <CodeBlockLineContext.Provider
          // biome-ignore lint/suspicious/noArrayIndexKey: lines are positional
          key={index}
          value={{ numbered: lineNumbers, number: startLine + index }}
        >
          {line}
        </CodeBlockLineContext.Provider>
      ))}
    </View>
  )
  return (
    <View className={cn("min-h-0 min-w-0", className)} {...props}>
      {wrap ? (
        body
      ) : (
        <ScrollView horizontal showsHorizontalScrollIndicator={false}>
          {body}
        </ScrollView>
      )}
    </View>
  )
}

const LINE_MARKS: Record<string, string | undefined> = {
  added: "+",
  removed: "−",
}

const codeBlockLineVariants = cva("relative min-w-full flex-row", {
  variants: {
    variant: {
      default: "",
      added: "bg-foreground-5",
      removed: "bg-destructive-10",
      highlighted: "bg-foreground-5",
    },
  },
  defaultVariants: {
    variant: "default",
  },
})

function CodeBlockLine({
  className,
  variant = "default",
  children,
  ...props
}: ViewProps & VariantProps<typeof codeBlockLineVariants>) {
  const { size } = React.useContext(CodeBlockContext)
  const { number, numbered } = React.useContext(CodeBlockLineContext)
  const mark = variant ? LINE_MARKS[variant] : undefined
  return (
    <View
      className={cn(
        codeBlockLineVariants({ variant }),
        size === "sm" ? "px-3" : "px-4",
        className
      )}
      {...props}
    >
      {mark ? (
        <Text
          accessible={false}
          className={cn(
            "absolute start-1",
            variant === "removed" ? "text-destructive" : "text-muted-foreground"
          )}
        >
          {mark}
        </Text>
      ) : null}
      {numbered ? (
        <Text
          accessible={false}
          className="me-3 min-w-6 text-end text-muted-foreground"
        >
          {number}
        </Text>
      ) : null}
      <Text className="min-w-0 shrink">{children ?? " "}</Text>
    </View>
  )
}

function CodeBlockFooter({ className, ...props }: ViewProps) {
  const { size } = React.useContext(CodeBlockContext)
  return (
    <TextClassContext.Provider value="font-sans text-xs text-muted-foreground">
      <View
        className={cn(
          "flex-row items-center gap-2",
          size === "sm" ? "px-3 pb-3" : "px-4 pb-4",
          className
        )}
        {...props}
      />
    </TextClassContext.Provider>
  )
}

export {
  CodeBlock,
  CodeBlockActions,
  CodeBlockContent,
  CodeBlockCopyButton,
  CodeBlockFooter,
  CodeBlockHeader,
  CodeBlockLine,
  CodeBlockTitle,
}
```

4. Update the import paths to match your project.

**StyleSheet**

1. Install the dependencies:

```bash title="Terminal"
npx expo install expo-clipboard phosphor-react-native react-native-svg
```

2. Add the pieces it builds on: [Icon](https://suiss.app/ui/docs/components/icon), [Text](https://suiss.app/ui/docs/components/text).

3. Copy the source into `components/ui/code-block.tsx`:

```tsx title="stylesheet/components/ui/code-block.tsx"
import { setStringAsync } from "expo-clipboard"
import { CheckIcon, CopyIcon } from "phosphor-react-native"
import * as React from "react"
import {
  Pressable,
  ScrollView,
  StyleSheet,
  type TextStyle,
  View,
  type ViewProps,
} from "react-native"
import { Icon, IconSizeContext } from "@/components/ui/icon"
import { Text, TextStyleContext } from "@/components/ui/text"
import {
  fontSize,
  radius,
  type ThemeColors,
  useTheme,
} from "@/components/ui/theme"

type CodeBlockSize = "default" | "sm"
type CodeBlockVariant = "default" | "destructive"
type CodeBlockLineVariant = "default" | "added" | "removed" | "highlighted"
type TokenKind = "plain" | "keyword" | "string" | "number" | "comment"

const KEYWORDS: Record<string, string[]> = {
  script: [
    "as",
    "async",
    "await",
    "break",
    "case",
    "catch",
    "class",
    "const",
    "continue",
    "default",
    "delete",
    "do",
    "else",
    "export",
    "extends",
    "false",
    "finally",
    "for",
    "from",
    "function",
    "if",
    "import",
    "in",
    "instanceof",
    "interface",
    "let",
    "new",
    "null",
    "of",
    "return",
    "satisfies",
    "static",
    "super",
    "switch",
    "this",
    "throw",
    "true",
    "try",
    "type",
    "typeof",
    "undefined",
    "var",
    "void",
    "while",
    "yield",
  ],
  json: ["true", "false", "null"],
  shell: [
    "cd",
    "do",
    "done",
    "echo",
    "elif",
    "else",
    "esac",
    "export",
    "fi",
    "for",
    "if",
    "in",
    "npm",
    "npx",
    "pnpm",
    "then",
    "while",
  ],
  python: [
    "and",
    "as",
    "class",
    "def",
    "elif",
    "else",
    "False",
    "for",
    "from",
    "if",
    "import",
    "in",
    "is",
    "None",
    "not",
    "or",
    "pass",
    "return",
    "True",
    "while",
    "with",
    "yield",
  ],
  swift: [
    "case",
    "class",
    "else",
    "enum",
    "extension",
    "false",
    "for",
    "func",
    "guard",
    "if",
    "import",
    "in",
    "let",
    "nil",
    "private",
    "protocol",
    "public",
    "return",
    "self",
    "static",
    "struct",
    "switch",
    "true",
    "var",
    "where",
    "while",
  ],
  kotlin: [
    "class",
    "data",
    "else",
    "enum",
    "false",
    "for",
    "fun",
    "if",
    "import",
    "in",
    "is",
    "null",
    "object",
    "package",
    "private",
    "return",
    "true",
    "val",
    "var",
    "when",
    "while",
  ],
}

const LANGUAGES: Record<string, keyof typeof KEYWORDS> = {
  bash: "shell",
  js: "script",
  javascript: "script",
  json: "json",
  jsx: "script",
  kotlin: "kotlin",
  kt: "kotlin",
  py: "python",
  python: "python",
  sh: "shell",
  shell: "shell",
  swift: "swift",
  ts: "script",
  tsx: "script",
  typescript: "script",
  yaml: "shell",
  yml: "shell",
}

const HASH_COMMENTS = new Set(["shell", "python"])
const TOKEN =
  /("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`)|(\b\d+(?:\.\d+)?\b)|([A-Za-z_$][\w$]*)/g
const TRAILING_NEWLINE = /\n$/

function tokenize(line: string, language?: string) {
  const group = language ? LANGUAGES[language.toLowerCase()] : undefined
  if (!group) return [{ kind: "plain" as TokenKind, text: line }]
  const keywords = new Set(KEYWORDS[group])
  const marker = HASH_COMMENTS.has(group) ? "#" : "//"
  const comment = line.indexOf(marker)
  const code = comment >= 0 ? line.slice(0, comment) : line
  const tokens: { kind: TokenKind; text: string }[] = []
  let last = 0
  for (const match of code.matchAll(TOKEN)) {
    const [text, string, number, word] = match
    const index = match.index ?? 0
    if (index > last)
      tokens.push({ kind: "plain", text: code.slice(last, index) })
    let kind: TokenKind = "plain"
    if (string) kind = "string"
    else if (number) kind = "number"
    else if (word && keywords.has(word)) kind = "keyword"
    tokens.push({ kind, text })
    last = index + text.length
  }
  if (last < code.length) tokens.push({ kind: "plain", text: code.slice(last) })
  if (comment >= 0) tokens.push({ kind: "comment", text: line.slice(comment) })
  return tokens
}

const MONO = "Menlo"

function tokenColor(kind: TokenKind, colors: ThemeColors) {
  if (kind === "keyword") return colors.chart3
  if (kind === "string") return colors.chart2
  if (kind === "number") return colors.chart4
  if (kind === "comment") return colors.mutedForeground
  return undefined
}

function lineBackground(variant: CodeBlockLineVariant, colors: ThemeColors) {
  if (variant === "removed") return `${colors.destructive}1a`
  if (variant === "default") return undefined
  return `${colors.foreground}0d`
}

const CodeBlockContext = React.createContext<{
  size: CodeBlockSize
  variant: CodeBlockVariant
}>({ size: "default", variant: "default" })

const CodeBlockLineContext = React.createContext<{
  number?: number
  numbered: boolean
}>({ numbered: false })

const LINE_MARKS: Partial<Record<CodeBlockLineVariant, string>> = {
  added: "+",
  removed: "−",
}

const styles = StyleSheet.create({
  root: {
    width: "100%",
    minWidth: 0,
    overflow: "hidden",
    borderWidth: StyleSheet.hairlineWidth,
    borderRadius: radius["2xl"],
  },
  rootSm: { borderRadius: radius.xl },
  header: { flexDirection: "row", alignItems: "center", gap: 8 },
  actions: {
    marginStart: "auto",
    flexDirection: "row",
    alignItems: "center",
    gap: 4,
  },
  copy: {
    width: 24,
    height: 24,
    alignItems: "center",
    justifyContent: "center",
    borderRadius: radius.md,
  },
  content: { minHeight: 0, minWidth: 0 },
  body: { minWidth: "100%" },
  line: { position: "relative", minWidth: "100%", flexDirection: "row" },
  mark: { position: "absolute", start: 4 },
  number: { marginEnd: 12, minWidth: 24, textAlign: "right" },
  code: { minWidth: 0, flexShrink: 1 },
  footer: { flexDirection: "row", alignItems: "center", gap: 8 },
})

function CodeBlock({
  style,
  variant = "default",
  size = "default",
  ...props
}: ViewProps & { variant?: CodeBlockVariant; size?: CodeBlockSize }) {
  const { colors, isDark } = useTheme()
  const destructive = variant === "destructive"
  const text: TextStyle = {
    ...(size === "sm" ? fontSize.xs : fontSize.sm),
    fontFamily: MONO,
    color: destructive ? colors.destructive : colors.foreground,
  }
  return (
    <CodeBlockContext.Provider value={{ size, variant }}>
      <TextStyleContext.Provider value={text}>
        <View
          style={[
            styles.root,
            size === "sm" && styles.rootSm,
            destructive
              ? {
                  backgroundColor: `${colors.destructive}${isDark ? "1a" : "0d"}`,
                  borderColor: `${colors.destructive}33`,
                }
              : {
                  backgroundColor: colors.muted,
                  borderColor: `${colors.foreground}${isDark ? "1a" : "0d"}`,
                },
            style,
          ]}
          {...props}
        />
      </TextStyleContext.Provider>
    </CodeBlockContext.Provider>
  )
}

function CodeBlockHeader({ style, ...props }: ViewProps) {
  const { size } = React.useContext(CodeBlockContext)
  return (
    <IconSizeContext.Provider value={16}>
      <View
        style={[
          styles.header,
          size === "sm"
            ? { paddingHorizontal: 12, paddingTop: 12 }
            : { paddingHorizontal: 16, paddingTop: 16 },
          style,
        ]}
        {...props}
      />
    </IconSizeContext.Provider>
  )
}

function CodeBlockTitle({
  style,
  ...props
}: React.ComponentProps<typeof Text>) {
  const { colors } = useTheme()
  const { size, variant } = React.useContext(CodeBlockContext)
  return (
    <Text
      numberOfLines={1}
      style={[
        size === "sm" ? fontSize.xs : fontSize.sm,
        {
          minWidth: 0,
          flexShrink: 1,
          fontFamily: undefined,
          fontWeight: "500",
          color:
            variant === "destructive" ? colors.destructive : colors.foreground,
        },
        style,
      ]}
      {...props}
    />
  )
}

function CodeBlockActions({ style, ...props }: ViewProps) {
  return <View style={[styles.actions, style]} {...props} />
}

function CodeBlockCopyButton({
  value,
  style,
  onPress,
  ...props
}: Omit<React.ComponentProps<typeof Pressable>, "children" | "style"> & {
  value: string
  style?: ViewProps["style"]
}) {
  const { colors } = useTheme()
  const [copied, setCopied] = React.useState(false)

  React.useEffect(() => {
    if (!copied) return
    const timeout = setTimeout(() => setCopied(false), 2000)
    return () => clearTimeout(timeout)
  }, [copied])

  return (
    <Pressable
      accessibilityRole="button"
      accessibilityLabel={copied ? "Copied" : "Copy"}
      hitSlop={8}
      style={({ pressed }) => [
        styles.copy,
        pressed && { backgroundColor: `${colors.foreground}1a` },
        style,
      ]}
      onPress={(event) => {
        onPress?.(event)
        setStringAsync(value)
          .then(() => setCopied(true))
          .catch(() => undefined)
      }}
      {...props}
    >
      <Icon
        as={copied ? CheckIcon : CopyIcon}
        size={14}
        color={colors.mutedForeground}
      />
    </Pressable>
  )
}

function CodeBlockContent({
  style,
  children,
  wrap = false,
  lineNumbers = false,
  startLine = 1,
  language,
  ...props
}: ViewProps & {
  wrap?: boolean
  lineNumbers?: boolean
  startLine?: number
  language?: string
}) {
  const { colors } = useTheme()
  const { size } = React.useContext(CodeBlockContext)
  const lines =
    typeof children === "string"
      ? children
          .replace(TRAILING_NEWLINE, "")
          .split("\n")
          .map((line, index) => (
            // biome-ignore lint/suspicious/noArrayIndexKey: lines are positional
            <CodeBlockLine key={index}>
              {tokenize(line, language).map((token, position) => (
                <Text
                  // biome-ignore lint/suspicious/noArrayIndexKey: tokens are positional
                  key={position}
                  style={{ color: tokenColor(token.kind, colors) }}
                >
                  {token.text}
                </Text>
              ))}
            </CodeBlockLine>
          ))
      : React.Children.toArray(children)
  const body = (
    <View style={[styles.body, { paddingVertical: size === "sm" ? 12 : 16 }]}>
      {lines.map((line, index) => (
        <CodeBlockLineContext.Provider
          // biome-ignore lint/suspicious/noArrayIndexKey: lines are positional
          key={index}
          value={{ numbered: lineNumbers, number: startLine + index }}
        >
          {line}
        </CodeBlockLineContext.Provider>
      ))}
    </View>
  )
  return (
    <View style={[styles.content, style]} {...props}>
      {wrap ? (
        body
      ) : (
        <ScrollView horizontal showsHorizontalScrollIndicator={false}>
          {body}
        </ScrollView>
      )}
    </View>
  )
}

function CodeBlockLine({
  style,
  variant = "default",
  children,
  ...props
}: ViewProps & { variant?: CodeBlockLineVariant }) {
  const { colors } = useTheme()
  const { size } = React.useContext(CodeBlockContext)
  const { number, numbered } = React.useContext(CodeBlockLineContext)
  const mark = LINE_MARKS[variant]
  const background = lineBackground(variant, colors)
  return (
    <View
      style={[
        styles.line,
        { paddingHorizontal: size === "sm" ? 12 : 16 },
        background ? { backgroundColor: background } : null,
        style,
      ]}
      {...props}
    >
      {mark ? (
        <Text
          accessible={false}
          style={[
            styles.mark,
            {
              color:
                variant === "removed"
                  ? colors.destructive
                  : colors.mutedForeground,
            },
          ]}
        >
          {mark}
        </Text>
      ) : null}
      {numbered ? (
        <Text
          accessible={false}
          style={[styles.number, { color: colors.mutedForeground }]}
        >
          {number}
        </Text>
      ) : null}
      <Text style={styles.code}>{children ?? " "}</Text>
    </View>
  )
}

function CodeBlockFooter({ style, ...props }: ViewProps) {
  const { colors } = useTheme()
  const { size } = React.useContext(CodeBlockContext)
  return (
    <TextStyleContext.Provider
      value={{ ...fontSize.xs, color: colors.mutedForeground }}
    >
      <View
        style={[
          styles.footer,
          size === "sm"
            ? { paddingHorizontal: 12, paddingBottom: 12 }
            : { paddingHorizontal: 16, paddingBottom: 16 },
          style,
        ]}
        {...props}
      />
    </TextStyleContext.Provider>
  )
}

export {
  CodeBlock,
  CodeBlockActions,
  CodeBlockContent,
  CodeBlockCopyButton,
  CodeBlockFooter,
  CodeBlockHeader,
  CodeBlockLine,
  CodeBlockTitle,
}
```

4. Update the import paths to match your project.

**SwiftUI**

1. Target iOS 17 or later.

2. Copy the theme files every view uses:

- `swift/Theme/Theme.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Theme/Theme.swift)

- `swift/Theme/Tokens.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Theme/Tokens.swift)

3. Copy the source and every file it uses:

```swift title="swift/Components/CodeBlock.swift"
import PhosphorSwift
import SwiftUI
import UIKit

public enum CodeBlockVariant: CaseIterable, Sendable {
  case `default`, destructive
}

public enum CodeBlockSize: CaseIterable, Sendable {
  case `default`, sm

  var padding: CGFloat { self == .sm ? 12 : 16 }
  var radius: CGFloat { self == .sm ? 12 : 18 }
  var text: TextStyle { self == .sm ? .xs : .sm }
}

public enum CodeBlockLineVariant: Sendable {
  case `default`, added, removed, highlighted
}

public struct CodeBlockLine: Identifiable, Sendable {
  public let id = UUID()
  public let text: String
  public let variant: CodeBlockLineVariant

  public init(_ text: String, variant: CodeBlockLineVariant = .default) {
    self.text = text
    self.variant = variant
  }
}

extension EnvironmentValues {
  @Entry var codeBlockVariant: CodeBlockVariant = .default
  @Entry var codeBlockSize: CodeBlockSize = .default
}

public struct CodeBlock<Content: View>: View {
  let variant: CodeBlockVariant
  let size: CodeBlockSize
  let content: Content

  public init(
    _ variant: CodeBlockVariant = .default,
    size: CodeBlockSize = .default,
    @ViewBuilder content: () -> Content
  ) {
    self.variant = variant
    self.size = size
    self.content = content()
  }

  public var body: some View {
    let destructive = variant == .destructive
    VStack(alignment: .leading, spacing: 0) {
      content
    }
    .frame(maxWidth: .infinity, alignment: .leading)
    .background(destructive ? EsuissColor.destructive.opacity(0.06) : EsuissColor.muted)
    .clipShape(.rect(cornerRadius: size.radius))
    .overlay {
      RoundedRectangle(cornerRadius: size.radius)
        .strokeBorder(destructive ? EsuissColor.destructive.opacity(0.18) : EsuissColor.foreground.opacity(0.06), lineWidth: 1)
    }
    .environment(\.codeBlockVariant, variant)
    .environment(\.codeBlockSize, size)
  }
}

public struct CodeBlockHeader<Content: View>: View {
  let content: Content

  @Environment(\.codeBlockSize) private var size
  @Environment(\.codeBlockVariant) private var variant

  public init(@ViewBuilder content: () -> Content) {
    self.content = content()
  }

  public var body: some View {
    HStack(spacing: 8) {
      content
    }
    .foregroundStyle(variant == .destructive ? EsuissColor.destructive : EsuissColor.mutedForeground)
    .padding(.horizontal, size.padding)
    .padding(.top, size.padding)
  }
}

public struct CodeBlockTitle: View {
  let text: Text

  @Environment(\.codeBlockSize) private var size
  @Environment(\.codeBlockVariant) private var variant

  public init(_ title: LocalizedStringKey) {
    self.text = Text(title)
  }

  public init(verbatim title: String) {
    self.text = Text(verbatim: title)
  }

  public var body: some View {
    text
      .scaledTextStyle(size.text, weight: .medium)
      .foregroundStyle(variant == .destructive ? EsuissColor.destructive : EsuissColor.foreground)
      .lineLimit(1)
  }
}

public struct CodeBlockActions<Content: View>: View {
  let content: Content

  public init(@ViewBuilder content: () -> Content) {
    self.content = content()
  }

  public var body: some View {
    HStack(spacing: 4) {
      content
    }
    .frame(maxWidth: .infinity, alignment: .trailing)
  }
}

public struct CodeBlockCopyButton: View {
  let value: String

  @State private var copied = false

  public init(value: String) {
    self.value = value
  }

  public var body: some View {
    Button {
      UIPasteboard.general.string = value
      copied = true
      Task {
        try? await Task.sleep(for: .seconds(2))
        copied = false
      }
    } label: {
      Group {
        if copied { Ph.check.regular } else { Ph.copy.regular }
      }
      .frame(width: 14, height: 14)
      .foregroundStyle(EsuissColor.mutedForeground)
      .frame(width: 24, height: 24)
      .contentShape(Rectangle())
    }
    .buttonStyle(.plain)
    .accessibilityLabel(copied ? "Copied" : "Copy")
  }
}

public struct CodeBlockContent: View {
  let lines: [CodeBlockLine]
  let language: String?
  let wrap: Bool
  let lineNumbers: Bool
  let startLine: Int

  @Environment(\.codeBlockSize) private var size
  @Environment(\.codeBlockVariant) private var variant

  public init(
    _ code: String,
    language: String? = nil,
    wrap: Bool = false,
    lineNumbers: Bool = false,
    startLine: Int = 1
  ) {
    let trimmed = code.hasSuffix("\n") ? String(code.dropLast()) : code
    self.lines = trimmed.components(separatedBy: "\n").map { CodeBlockLine($0) }
    self.language = language
    self.wrap = wrap
    self.lineNumbers = lineNumbers
    self.startLine = startLine
  }

  public init(
    lines: [CodeBlockLine],
    language: String? = nil,
    wrap: Bool = false,
    lineNumbers: Bool = false,
    startLine: Int = 1
  ) {
    self.lines = lines
    self.language = language
    self.wrap = wrap
    self.lineNumbers = lineNumbers
    self.startLine = startLine
  }

  public var body: some View {
    let body = VStack(alignment: .leading, spacing: 0) {
      ForEach(Array(lines.enumerated()), id: \.element.id) { index, line in
        row(line, number: startLine + index)
      }
    }
    .padding(.vertical, size.padding)
    Group {
      if wrap {
        body
      } else {
        ScrollView(.horizontal, showsIndicators: false) { body }
      }
    }
    .font(.system(size: size == .sm ? 12 : 14, design: .monospaced))
    .foregroundStyle(variant == .destructive ? EsuissColor.destructive : EsuissColor.foreground)
  }

  @ViewBuilder
  private func row(_ line: CodeBlockLine, number: Int) -> some View {
    HStack(alignment: .firstTextBaseline, spacing: 12) {
      if lineNumbers {
        Text(verbatim: "\(number)")
          .foregroundStyle(EsuissColor.mutedForeground)
          .frame(minWidth: 24, alignment: .trailing)
          .accessibilityHidden(true)
      }
      Text(CodeHighlighter.highlight(line.text.isEmpty ? " " : line.text, language: language))
        .fixedSize(horizontal: !wrap, vertical: true)
        .frame(maxWidth: wrap ? .infinity : nil, alignment: .leading)
    }
    .padding(.horizontal, size.padding)
    .frame(maxWidth: .infinity, alignment: .leading)
    .background(background(line.variant))
    .overlay(alignment: .leading) {
      if let mark = mark(line.variant) {
        Text(verbatim: mark)
          .foregroundStyle(line.variant == .removed ? EsuissColor.destructive : EsuissColor.mutedForeground)
          .padding(.leading, 4)
          .accessibilityHidden(true)
      }
    }
  }

  private func mark(_ variant: CodeBlockLineVariant) -> String? {
    switch variant {
    case .added: "+"
    case .removed: "−"
    default: nil
    }
  }

  private func background(_ variant: CodeBlockLineVariant) -> Color {
    switch variant {
    case .default: .clear
    case .removed: EsuissColor.destructive.opacity(0.1)
    case .added, .highlighted: EsuissColor.foreground.opacity(0.05)
    }
  }
}

public struct CodeBlockFooter<Content: View>: View {
  let content: Content

  @Environment(\.codeBlockSize) private var size

  public init(@ViewBuilder content: () -> Content) {
    self.content = content()
  }

  public var body: some View {
    HStack(spacing: 8) {
      content
    }
    .scaledTextStyle(.xs)
    .foregroundStyle(EsuissColor.mutedForeground)
    .padding(.horizontal, size.padding)
    .padding(.bottom, size.padding)
  }
}

enum CodeHighlighter {
  private static let keywords: [String: Set<String>] = [
    "script": ["as", "async", "await", "break", "case", "catch", "class", "const", "continue", "default", "delete", "do", "else", "export", "extends", "false", "finally", "for", "from", "function", "if", "import", "in", "instanceof", "interface", "let", "new", "null", "of", "return", "satisfies", "static", "super", "switch", "this", "throw", "true", "try", "type", "typeof", "undefined", "var", "void", "while", "yield"],
    "json": ["true", "false", "null"],
    "shell": ["cd", "do", "done", "echo", "elif", "else", "esac", "export", "fi", "for", "if", "in", "npm", "npx", "pnpm", "then", "while"],
    "python": ["and", "as", "class", "def", "elif", "else", "False", "for", "from", "if", "import", "in", "is", "None", "not", "or", "pass", "return", "True", "while", "with", "yield"],
    "swift": ["case", "class", "else", "enum", "extension", "false", "for", "func", "guard", "if", "import", "in", "let", "nil", "private", "protocol", "public", "return", "self", "static", "struct", "switch", "true", "var", "where", "while"],
    "kotlin": ["class", "data", "else", "enum", "false", "for", "fun", "if", "import", "in", "is", "null", "object", "package", "private", "return", "true", "val", "var", "when", "while"],
  ]

  private static let languages: [String: String] = [
    "bash": "shell", "js": "script", "javascript": "script", "json": "json", "jsx": "script",
    "kotlin": "kotlin", "kt": "kotlin", "py": "python", "python": "python", "sh": "shell",
    "shell": "shell", "swift": "swift", "ts": "script", "tsx": "script", "typescript": "script",
    "yaml": "shell", "yml": "shell",
  ]

  private static let token = try? NSRegularExpression(
    pattern: #"("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`)|(\b\d+(?:\.\d+)?\b)|([A-Za-z_$][\w$]*)"#
  )

  static func highlight(_ line: String, language: String?) -> AttributedString {
    guard let language, let group = languages[language.lowercased()], let token else {
      return AttributedString(line)
    }
    let words = keywords[group] ?? []
    let marker = group == "shell" || group == "python" ? "#" : "//"
    let comment = line.range(of: marker)
    let code = comment.map { String(line[..<$0.lowerBound]) } ?? line
    var result = AttributedString()
    var last = code.startIndex
    let nsRange = NSRange(code.startIndex..., in: code)
    for match in token.matches(in: code, range: nsRange) {
      guard let range = Range(match.range, in: code) else { continue }
      if range.lowerBound > last { result += AttributedString(String(code[last..<range.lowerBound])) }
      var piece = AttributedString(String(code[range]))
      if match.range(at: 1).location != NSNotFound {
        piece.foregroundColor = EsuissColor.chart2
      } else if match.range(at: 2).location != NSNotFound {
        piece.foregroundColor = EsuissColor.chart4
      } else if words.contains(String(code[range])) {
        piece.foregroundColor = EsuissColor.chart3
      }
      result += piece
      last = range.upperBound
    }
    if last < code.endIndex { result += AttributedString(String(code[last...])) }
    if let comment {
      var piece = AttributedString(String(line[comment.lowerBound...]))
      piece.foregroundColor = EsuissColor.mutedForeground
      result += piece
    }
    return result
  }
}
```

4. Add the files to your app target.

**Jetpack Compose**

1. Set `minSdk` to 24 or higher and add the dependencies:

```kotlin title="build.gradle.kts"
implementation(platform("androidx.compose:compose-bom:2026.09.00"))
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.ui:ui")
```

2. Copy the theme files every view uses:

- `kotlin/theme/Theme.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/theme/Theme.kt)

- `kotlin/theme/Tokens.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/theme/Tokens.kt)

3. Copy the source and every file it uses:

```kt title="kotlin/components/CodeBlock.kt"
package com.esuiss.ui.components

import android.content.ClipData
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.Check
import com.adamglin.phosphoricons.regular.Copy
import com.esuiss.ui.theme.Colors
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Typography
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

enum class CodeBlockVariant { Default, Destructive }

enum class CodeBlockSize(val padding: Dp, val radius: Dp) {
    Default(16.dp, 18.dp),
    Sm(12.dp, 12.dp),
}

enum class CodeBlockLineVariant { Default, Added, Removed, Highlighted }

data class CodeBlockLine(val text: String, val variant: CodeBlockLineVariant = CodeBlockLineVariant.Default)

private data class CodeBlockStyle(val variant: CodeBlockVariant, val size: CodeBlockSize)

private val LocalCodeBlock = staticCompositionLocalOf { CodeBlockStyle(CodeBlockVariant.Default, CodeBlockSize.Default) }

@Composable
fun CodeBlock(
    modifier: Modifier = Modifier,
    variant: CodeBlockVariant = CodeBlockVariant.Default,
    size: CodeBlockSize = CodeBlockSize.Default,
    content: @Composable ColumnScope.() -> Unit,
) {
    val colors = EsuissTheme.colors
    val destructive = variant == CodeBlockVariant.Destructive
    val shape = RoundedCornerShape(size.radius)
    Column(
        modifier = modifier
            .fillMaxWidth()
            .clip(shape)
            .background(if (destructive) colors.destructive.copy(alpha = 0.06f) else colors.muted)
            .border(1.dp, if (destructive) colors.destructive.copy(alpha = 0.18f) else colors.foreground.copy(alpha = 0.06f), shape),
    ) {
        CompositionLocalProvider(LocalCodeBlock provides CodeBlockStyle(variant, size)) {
            content()
        }
    }
}

@Composable
fun CodeBlockHeader(
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit,
) {
    val style = LocalCodeBlock.current
    val colors = EsuissTheme.colors
    val tone = if (style.variant == CodeBlockVariant.Destructive) colors.destructive else colors.mutedForeground
    Row(
        modifier = modifier
            .fillMaxWidth()
            .padding(start = style.size.padding, end = style.size.padding, top = style.size.padding),
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        verticalAlignment = Alignment.CenterVertically,
    ) {
        CompositionLocalProvider(LocalContentColor provides tone, LocalIconSize provides 16.dp) {
            content()
        }
    }
}

@Composable
fun RowScope.CodeBlockTitle(text: String, modifier: Modifier = Modifier) {
    val style = LocalCodeBlock.current
    val colors = EsuissTheme.colors
    val base = if (style.size == CodeBlockSize.Sm) Typography.xs else Typography.sm
    BasicText(
        text,
        modifier = modifier.weight(1f, fill = false),
        style = base.copy(
            fontWeight = FontWeight.Medium,
            color = if (style.variant == CodeBlockVariant.Destructive) colors.destructive else colors.foreground,
        ),
        maxLines = 1,
        overflow = TextOverflow.Ellipsis,
    )
}

@Composable
fun RowScope.CodeBlockActions(
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit,
) {
    Row(
        modifier = modifier.weight(1f),
        horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.End),
        verticalAlignment = Alignment.CenterVertically,
        content = content,
    )
}

@Composable
fun CodeBlockCopyButton(value: String, modifier: Modifier = Modifier) {
    val clipboard = LocalClipboard.current
    val scope = rememberCoroutineScope()
    var copied by remember { mutableStateOf(false) }
    LaunchedEffect(copied) {
        if (copied) {
            delay(2000)
            copied = false
        }
    }
    Box(
        modifier = modifier
            .size(24.dp)
            .clip(RoundedCornerShape(6.dp))
            .semantics { contentDescription = if (copied) "Copied" else "Copy" }
            .clickable(role = Role.Button) {
                scope.launch {
                    clipboard.setClipEntry(ClipEntry(ClipData.newPlainText("code", value)))
                    copied = true
                }
            },
        contentAlignment = Alignment.Center,
    ) {
        Icon(
            if (copied) PhosphorIcons.Regular.Check else PhosphorIcons.Regular.Copy,
            contentDescription = null,
            size = 14.dp,
            tint = EsuissTheme.colors.mutedForeground,
        )
    }
}

@Composable
fun CodeBlockContent(
    code: String,
    modifier: Modifier = Modifier,
    language: String? = null,
    wrap: Boolean = false,
    lineNumbers: Boolean = false,
    startLine: Int = 1,
) {
    CodeBlockContent(
        lines = code.removeSuffix("\n").split("\n").map { CodeBlockLine(it) },
        modifier = modifier,
        language = language,
        wrap = wrap,
        lineNumbers = lineNumbers,
        startLine = startLine,
    )
}

@Composable
fun CodeBlockContent(
    lines: List<CodeBlockLine>,
    modifier: Modifier = Modifier,
    language: String? = null,
    wrap: Boolean = false,
    lineNumbers: Boolean = false,
    startLine: Int = 1,
) {
    val style = LocalCodeBlock.current
    val colors = EsuissTheme.colors
    val text = (if (style.size == CodeBlockSize.Sm) Typography.xs else Typography.sm).copy(
        fontFamily = FontFamily.Monospace,
        color = if (style.variant == CodeBlockVariant.Destructive) colors.destructive else colors.foreground,
    )
    val scroll = rememberScrollState()
    Column(
        modifier = modifier
            .fillMaxWidth()
            .then(if (wrap) Modifier else Modifier.horizontalScroll(scroll))
            .padding(vertical = style.size.padding),
    ) {
        lines.forEachIndexed { index, line ->
            val background = when (line.variant) {
                CodeBlockLineVariant.Default -> Color.Transparent
                CodeBlockLineVariant.Removed -> colors.destructive.copy(alpha = 0.1f)
                else -> colors.foreground.copy(alpha = 0.05f)
            }
            Box(Modifier.background(background)) {
                Row(
                    modifier = Modifier.padding(horizontal = style.size.padding),
                    horizontalArrangement = Arrangement.spacedBy(12.dp),
                ) {
                    if (lineNumbers) {
                        BasicText(
                            "${startLine + index}",
                            modifier = Modifier.widthIn(min = 24.dp).clearAndSetSemantics {},
                            style = text.copy(color = colors.mutedForeground, textAlign = TextAlign.End),
                        )
                    }
                    BasicText(
                        CodeHighlighter.highlight(line.text.ifEmpty { " " }, language, colors),
                        style = text,
                        softWrap = wrap,
                    )
                }
                val mark = when (line.variant) {
                    CodeBlockLineVariant.Added -> "+"
                    CodeBlockLineVariant.Removed -> "−"
                    else -> null
                }
                if (mark != null) {
                    BasicText(
                        mark,
                        modifier = Modifier.padding(start = 4.dp).clearAndSetSemantics {},
                        style = text.copy(
                            color = if (line.variant == CodeBlockLineVariant.Removed) colors.destructive else colors.mutedForeground,
                        ),
                    )
                }
            }
        }
    }
}

@Composable
fun CodeBlockFooter(
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit,
) {
    val style = LocalCodeBlock.current
    val colors = EsuissTheme.colors
    Row(
        modifier = modifier
            .fillMaxWidth()
            .padding(start = style.size.padding, end = style.size.padding, bottom = style.size.padding),
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        verticalAlignment = Alignment.CenterVertically,
    ) {
        CompositionLocalProvider(
            LocalContentStyle provides Typography.xs.copy(color = colors.mutedForeground),
            LocalContentColor provides colors.mutedForeground,
        ) {
            content()
        }
    }
}

private object CodeHighlighter {
    private val keywords = mapOf(
        "script" to setOf("as", "async", "await", "break", "case", "catch", "class", "const", "continue", "default", "delete", "do", "else", "export", "extends", "false", "finally", "for", "from", "function", "if", "import", "in", "instanceof", "interface", "let", "new", "null", "of", "return", "satisfies", "static", "super", "switch", "this", "throw", "true", "try", "type", "typeof", "undefined", "var", "void", "while", "yield"),
        "json" to setOf("true", "false", "null"),
        "shell" to setOf("cd", "do", "done", "echo", "elif", "else", "esac", "export", "fi", "for", "if", "in", "npm", "npx", "pnpm", "then", "while"),
        "python" to setOf("and", "as", "class", "def", "elif", "else", "False", "for", "from", "if", "import", "in", "is", "None", "not", "or", "pass", "return", "True", "while", "with", "yield"),
        "swift" to setOf("case", "class", "else", "enum", "extension", "false", "for", "func", "guard", "if", "import", "in", "let", "nil", "private", "protocol", "public", "return", "self", "static", "struct", "switch", "true", "var", "where", "while"),
        "kotlin" to setOf("class", "data", "else", "enum", "false", "for", "fun", "if", "import", "in", "is", "null", "object", "package", "private", "return", "true", "val", "var", "when", "while"),
    )

    private val languages = mapOf(
        "bash" to "shell", "js" to "script", "javascript" to "script", "json" to "json", "jsx" to "script",
        "kotlin" to "kotlin", "kt" to "kotlin", "py" to "python", "python" to "python", "sh" to "shell",
        "shell" to "shell", "swift" to "swift", "ts" to "script", "tsx" to "script", "typescript" to "script",
        "yaml" to "shell", "yml" to "shell",
    )

    private val token = Regex("""("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`)|(\b\d+(?:\.\d+)?\b)|([A-Za-z_$][\w$]*)""")

    fun highlight(line: String, language: String?, colors: Colors): AnnotatedString {
        val group = language?.lowercase()?.let { languages[it] } ?: return AnnotatedString(line)
        val words = keywords[group].orEmpty()
        val marker = if (group == "shell" || group == "python") "#" else "//"
        val comment = line.indexOf(marker)
        val code = if (comment >= 0) line.substring(0, comment) else line
        return buildAnnotatedString {
            var last = 0
            for (match in token.findAll(code)) {
                if (match.range.first > last) append(code.substring(last, match.range.first))
                val color = when {
                    match.groups[1] != null -> colors.chart2
                    match.groups[2] != null -> colors.chart4
                    match.value in words -> colors.chart3
                    else -> null
                }
                if (color != null) {
                    pushStyle(SpanStyle(color = color))
                    append(match.value)
                    pop()
                } else {
                    append(match.value)
                }
                last = match.range.last + 1
            }
            if (last < code.length) append(code.substring(last))
            if (comment >= 0) {
                pushStyle(SpanStyle(color = colors.mutedForeground))
                append(line.substring(comment))
                pop()
            }
        }
    }
}
```

- `kotlin/components/Button.kt`: see [Button](https://suiss.app/ui/docs/components/button.md)

- `kotlin/components/ButtonGroup.kt`: see [Button Group](https://suiss.app/ui/docs/components/button-group.md)

- `kotlin/components/Content.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/components/Content.kt)

- `kotlin/components/Icon.kt`: see [Icon](https://suiss.app/ui/docs/components/icon.md)

4. Update the `com.esuiss.ui` package names to match your project.

## Usage

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

```tsx
<CodeBlock>
  <CodeBlockHeader>
    <CodeBlockTitle>components/greeting.tsx</CodeBlockTitle>
    <CodeBlockActions>
      <CodeBlockCopyButton value={code} />
    </CodeBlockActions>
  </CodeBlockHeader>
  <CodeBlockContent language="tsx">{code}</CodeBlockContent>
</CodeBlock>
```

In SwiftUI and Compose, pass the code as a string or a list of `CodeBlockLine` values to `CodeBlockContent`. The copy button uses `expo-clipboard` in React Native, `UIPasteboard` in SwiftUI and the Compose clipboard on Android.

## Examples

### Basic

A single line with no header.

```tsx
import { CodeBlock, CodeBlockContent } from "@/components/ui/code-block"

export function CodeBlockBasicExample() {
  return (
    <CodeBlock>
      <CodeBlockContent>npx @esuiss/ui@latest add code-block</CodeBlockContent>
    </CodeBlock>
  )
}
```

```swift
import EsuissUI
import SwiftUI

struct CodeBlockBasicExample: View {
  var body: some View {
    CodeBlock {
      CodeBlockContent("npx @esuiss/ui@latest add code-block")
    }
  }
}
```

```kotlin
import androidx.compose.runtime.Composable
import com.esuiss.ui.components.CodeBlock
import com.esuiss.ui.components.CodeBlockContent

@Composable
fun CodeBlockBasicExample() {
    CodeBlock { CodeBlockContent("npx @esuiss/ui@latest add code-block") }
}
```

### Line numbers

Numbers every line with `lineNumbers`.

```tsx
import { CodeBlock, CodeBlockContent } from "@/components/ui/code-block"

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

export function CodeBlockLineNumbersExample() {
  return (
    <CodeBlock>
      <CodeBlockContent language="ts" lineNumbers>
        {config}
      </CodeBlockContent>
    </CodeBlock>
  )
}
```

```swift
import EsuissUI
import SwiftUI

struct CodeBlockLineNumbersExample: View {
  var body: some View {
    CodeBlock {
      CodeBlockContent(config, language: "ts", lineNumbers: true)
    }
  }
}

let config = """
```

```kotlin
import androidx.compose.runtime.Composable
import com.esuiss.ui.components.CodeBlock
import com.esuiss.ui.components.CodeBlockContent

@Composable
fun CodeBlockLineNumbersExample() {
    CodeBlock { CodeBlockContent(CONFIG, language = "ts", lineNumbers = true) }
}
```

### Start line

Starts the numbering at `startLine` for an excerpt of a longer file.

```tsx
import { CodeBlock, CodeBlockContent, CodeBlockHeader, CodeBlockLine, CodeBlockTitle } from "@/components/ui/code-block"

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

export function CodeBlockStartLineExample() {
  return (
    <CodeBlock>
      <CodeBlockHeader>
        <CodeBlockTitle>next.config.ts</CodeBlockTitle>
      </CodeBlockHeader>
      <CodeBlockContent lineNumbers startLine={12}>
        <CodeBlockLine>{"  images: {"}</CodeBlockLine>
        <CodeBlockLine>{"    unoptimized: true,"}</CodeBlockLine>
        <CodeBlockLine>{"  },"}</CodeBlockLine>
      </CodeBlockContent>
    </CodeBlock>
  )
}
```

```swift
import EsuissUI
import SwiftUI

struct CodeBlockStartLineExample: View {
  var body: some View {
    CodeBlock {
      CodeBlockHeader { CodeBlockTitle(verbatim: "next.config.ts") }
      CodeBlockContent(
        lines: [CodeBlockLine("  images: {"), CodeBlockLine("    unoptimized: true,"), CodeBlockLine("  },")],
        lineNumbers: true,
        startLine: 12
      )
    }
  }
}

let config = """
```

```kotlin
import androidx.compose.runtime.Composable
import com.esuiss.ui.components.CodeBlock
import com.esuiss.ui.components.CodeBlockContent
import com.esuiss.ui.components.CodeBlockHeader
import com.esuiss.ui.components.CodeBlockLine
import com.esuiss.ui.components.CodeBlockTitle

@Composable
fun CodeBlockStartLine() {
    CodeBlock {
        CodeBlockHeader { CodeBlockTitle("next.config.ts") }
        CodeBlockContent(
            lines = listOf(CodeBlockLine("  images: {"), CodeBlockLine("    unoptimized: true,"), CodeBlockLine("  },")),
            lineNumbers = true,
            startLine = 12,
        )
    }
}
```

### Wrap

Wraps long lines instead of scrolling sideways.

```tsx
import { CodeBlock, CodeBlockContent } from "@/components/ui/code-block"

export function CodeBlockWrapExample() {
  return (
    <CodeBlock>
      <CodeBlockContent wrap>
        ERR_PNPM_OUTDATED_LOCKFILE Cannot install with
        &quot;frozen-lockfile&quot; because pnpm-lock.yaml is not up to date
        with package.json
      </CodeBlockContent>
    </CodeBlock>
  )
}
```

```swift
import EsuissUI
import SwiftUI

struct CodeBlockWrapExample: View {
  var body: some View {
    CodeBlock {
      CodeBlockContent(
        "ERR_PNPM_OUTDATED_LOCKFILE Cannot install with \"frozen-lockfile\" because pnpm-lock.yaml is not up to date with package.json",
        wrap: true
      )
    }
  }
}
```

```kotlin
import androidx.compose.runtime.Composable
import com.esuiss.ui.components.CodeBlock
import com.esuiss.ui.components.CodeBlockContent

@Composable
fun CodeBlockWrapExample() {
    CodeBlock {
        CodeBlockContent(
            "ERR_PNPM_OUTDATED_LOCKFILE Cannot install with \"frozen-lockfile\" because pnpm-lock.yaml is not up to date with package.json",
            wrap = true,
        )
    }
}
```

### Highlighted lines

`CodeBlockLine` with `variant="highlighted"` tints the lines that matter.

```tsx
import { CodeBlock, CodeBlockContent, CodeBlockLine } from "@/components/ui/code-block"

export function CodeBlockHighlightedLinesExample() {
  return (
    <CodeBlock>
      <CodeBlockContent lineNumbers>
        <CodeBlockLine>{"export default {"}</CodeBlockLine>
        <CodeBlockLine>{"  experimental: {"}</CodeBlockLine>
        <CodeBlockLine variant="highlighted">
          {"    reactCompiler: true,"}
        </CodeBlockLine>
        <CodeBlockLine variant="highlighted">
          {"    viewTransition: true,"}
        </CodeBlockLine>
        <CodeBlockLine>{"  },"}</CodeBlockLine>
        <CodeBlockLine>{"}"}</CodeBlockLine>
      </CodeBlockContent>
    </CodeBlock>
  )
}
```

```swift
import EsuissUI
import SwiftUI

struct CodeBlockHighlightedLinesExample: View {
  var body: some View {
    CodeBlock {
      CodeBlockContent(
        lines: [
          CodeBlockLine("export default {"),
          CodeBlockLine("  experimental: {"),
          CodeBlockLine("    reactCompiler: true,", variant: .highlighted),
          CodeBlockLine("    viewTransition: true,", variant: .highlighted),
          CodeBlockLine("  },"),
          CodeBlockLine("}"),
        ],
        lineNumbers: true
      )
    }
  }
}
```

```kotlin
import androidx.compose.runtime.Composable
import com.esuiss.ui.components.CodeBlock
import com.esuiss.ui.components.CodeBlockContent
import com.esuiss.ui.components.CodeBlockLine
import com.esuiss.ui.components.CodeBlockLineVariant

@Composable
fun CodeBlockHighlighted() {
    CodeBlock {
        CodeBlockContent(
            lines = listOf(
                CodeBlockLine("export default {"),
                CodeBlockLine("  experimental: {"),
                CodeBlockLine("    reactCompiler: true,", CodeBlockLineVariant.Highlighted),
                CodeBlockLine("    viewTransition: true,", CodeBlockLineVariant.Highlighted),
                CodeBlockLine("  },"),
                CodeBlockLine("}"),
            ),
            lineNumbers = true,
        )
    }
}
```

### Diff

`added` and `removed` lines get a tint and a + or − marker.

```tsx
import { CodeBlock, CodeBlockContent, CodeBlockHeader, CodeBlockLine, CodeBlockTitle } from "@/components/ui/code-block"
import { Text } from "@/components/ui/text"

const greeting = `export function Greeting({ name }: { name: string }) {
  // Say hello to the person signing in
  return <Text>Hello, {name}!</Text>
}`

export function CodeBlockDiffExample() {
  return (
    <CodeBlock>
      <CodeBlockHeader>
        <CodeBlockTitle>components/greeting.tsx</CodeBlockTitle>
      </CodeBlockHeader>
      <CodeBlockContent>
        <CodeBlockLine>
          {"export function Greeting({ name }: { name: string }) {"}
        </CodeBlockLine>
        <CodeBlockLine variant="removed">
          {"  return <Text>Hello, {name}</Text>"}
        </CodeBlockLine>
        <CodeBlockLine variant="added">
          {'  return <Text variant="muted">'}
        </CodeBlockLine>
        <CodeBlockLine variant="added">{"    Hello, {name}!"}</CodeBlockLine>
        <CodeBlockLine variant="added">{"  </Text>"}</CodeBlockLine>
        <CodeBlockLine>{"}"}</CodeBlockLine>
      </CodeBlockContent>
    </CodeBlock>
  )
}
```

```swift
import EsuissUI
import SwiftUI

struct CodeBlockDiffExample: View {
  var body: some View {
    CodeBlock {
      CodeBlockHeader { CodeBlockTitle(verbatim: "components/greeting.tsx") }
      CodeBlockContent(lines: [
        CodeBlockLine("export function Greeting({ name }: { name: string }) {"),
        CodeBlockLine("  return <Text>Hello, {name}</Text>", variant: .removed),
        CodeBlockLine("  return <Text variant=\"muted\">", variant: .added),
        CodeBlockLine("    Hello, {name}!", variant: .added),
        CodeBlockLine("  </Text>", variant: .added),
        CodeBlockLine("}"),
      ])
    }
  }
}

let greeting = """
```

```kotlin
import androidx.compose.runtime.Composable
import com.esuiss.ui.components.CodeBlock
import com.esuiss.ui.components.CodeBlockContent
import com.esuiss.ui.components.CodeBlockHeader
import com.esuiss.ui.components.CodeBlockLine
import com.esuiss.ui.components.CodeBlockLineVariant
import com.esuiss.ui.components.CodeBlockTitle
import com.esuiss.ui.components.Text

@Composable
fun CodeBlockDiff() {
    CodeBlock {
        CodeBlockHeader { CodeBlockTitle("components/greeting.tsx") }
        CodeBlockContent(
            lines = listOf(
                CodeBlockLine("export function Greeting({ name }: { name: string }) {"),
                CodeBlockLine("  return <Text>Hello, {name}</Text>", CodeBlockLineVariant.Removed),
                CodeBlockLine("  return <Text variant=\"muted\">", CodeBlockLineVariant.Added),
                CodeBlockLine("    Hello, {name}!", CodeBlockLineVariant.Added),
                CodeBlockLine("  </Text>", CodeBlockLineVariant.Added),
                CodeBlockLine("}"),
            ),
        )
    }
}
```

### Size

The `sm` size for code inside a message or a trace.

```tsx
import { View } from "react-native"
import { CodeBlock, CodeBlockContent } from "@/components/ui/code-block"

export function CodeBlockSizeExample() {
  return (
    <View className="gap-4">
      <CodeBlock>
        <CodeBlockContent>pnpm install</CodeBlockContent>
      </CodeBlock>
      <CodeBlock size="sm">
        <CodeBlockContent>pnpm install</CodeBlockContent>
      </CodeBlock>
    </View>
  )
}
```

```swift
import EsuissUI
import SwiftUI

struct CodeBlockSizeExample: View {
  var body: some View {
    VStack(spacing: 16) {
      CodeBlock { CodeBlockContent("pnpm install") }
      CodeBlock(size: .sm) { CodeBlockContent("pnpm install") }
    }
  }
}
```

```kotlin
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.CodeBlock
import com.esuiss.ui.components.CodeBlockContent
import com.esuiss.ui.components.CodeBlockSize

@Composable
fun CodeBlockSizes() {
    Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(16.dp)) {
        CodeBlock { CodeBlockContent("pnpm install") }
        CodeBlock(size = CodeBlockSize.Sm) { CodeBlockContent("pnpm install") }
    }
}
```

### Destructive

The `destructive` variant for errors.

```tsx
import { TerminalIcon } from "phosphor-react-native"
import { CodeBlock, CodeBlockContent, CodeBlockHeader, CodeBlockTitle } from "@/components/ui/code-block"
import { Icon } from "@/components/ui/icon"

export function CodeBlockDestructiveExample() {
  return (
    <CodeBlock variant="destructive" size="sm">
      <CodeBlockHeader>
        <Icon as={TerminalIcon} />
        <CodeBlockTitle>Failed to run pnpm lint</CodeBlockTitle>
      </CodeBlockHeader>
      <CodeBlockContent wrap>
        {"app/page.tsx:12:7 Unexpected any. Specify a different type."}
      </CodeBlockContent>
    </CodeBlock>
  )
}
```

```swift
import EsuissUI
import PhosphorSwift
import SwiftUI

struct CodeBlockDestructiveExample: View {
  var body: some View {
    CodeBlock(.destructive, size: .sm) {
      CodeBlockHeader {
        Ph.terminal.regular.frame(width: 16, height: 16)
        CodeBlockTitle("Failed to run pnpm lint")
      }
      CodeBlockContent("app/page.tsx:12:7 Unexpected any. Specify a different type.", wrap: true)
    }
  }
}
```

```kotlin
import androidx.compose.runtime.Composable
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.Terminal
import com.esuiss.ui.components.CodeBlock
import com.esuiss.ui.components.CodeBlockContent
import com.esuiss.ui.components.CodeBlockHeader
import com.esuiss.ui.components.CodeBlockSize
import com.esuiss.ui.components.CodeBlockTitle
import com.esuiss.ui.components.CodeBlockVariant
import com.esuiss.ui.components.Icon

@Composable
fun CodeBlockDestructive() {
    CodeBlock(variant = CodeBlockVariant.Destructive, size = CodeBlockSize.Sm) {
        CodeBlockHeader {
            Icon(PhosphorIcons.Regular.Terminal, contentDescription = null)
            CodeBlockTitle("Failed to run pnpm lint")
        }
        CodeBlockContent("app/page.tsx:12:7 Unexpected any. Specify a different type.", wrap = true)
    }
}
```

### Languages

The built-in highlighter colors TypeScript, JSON, shell, Swift and Kotlin with the chart colors of the theme.

```tsx
import { View } from "react-native"
import { CodeBlock, CodeBlockContent } from "@/components/ui/code-block"
import { Text } from "@/components/ui/text"

export function CodeBlockLanguagesExample() {
  return (
    <View className="gap-4">
      <CodeBlock size="sm">
        <CodeBlockContent language="bash">
          {"# Add the component\npnpm dlx @esuiss/ui add code-block"}
        </CodeBlockContent>
      </CodeBlock>
      <CodeBlock size="sm">
        <CodeBlockContent language="json">
          {'{ "name": "acme", "private": true, "version": "1.0.0" }'}
        </CodeBlockContent>
      </CodeBlock>
      <CodeBlock size="sm">
        <CodeBlockContent language="swift">
          {
            'struct Greeting: View {\n  let name: String\n  var body: some View { Text("Hello, \\(name)") }\n}'
          }
        </CodeBlockContent>
      </CodeBlock>
      <CodeBlock size="sm">
        <CodeBlockContent language="kotlin">
          {
            '@Composable\nfun Greeting(name: String) {\n  Text("Hello, $name")\n}'
          }
        </CodeBlockContent>
      </CodeBlock>
    </View>
  )
}
```

```swift
import EsuissUI
import SwiftUI

struct CodeBlockLanguagesExample: View {
  var body: some View {
    VStack(spacing: 16) {
      CodeBlock(size: .sm) { CodeBlockContent("# Add the component\npnpm dlx @esuiss/ui add code-block", language: "bash") }
      CodeBlock(size: .sm) { CodeBlockContent("{ \"name\": \"acme\", \"private\": true, \"version\": \"1.0.0\" }", language: "json") }
      CodeBlock(size: .sm) {
        CodeBlockContent("struct Greeting: View {\n  let name: String\n  var body: some View { Text(\"Hello, \\(name)\") }\n}", language: "swift")
      }
      CodeBlock(size: .sm) {
        CodeBlockContent("@Composable\nfun Greeting(name: String) {\n  Text(\"Hello, $name\")\n}", language: "kotlin")
      }
    }
  }
}
```

```kotlin
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.CodeBlock
import com.esuiss.ui.components.CodeBlockContent
import com.esuiss.ui.components.CodeBlockSize
import com.esuiss.ui.components.Text

@Composable
fun CodeBlockLanguages() {
    Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(16.dp)) {
        CodeBlock(size = CodeBlockSize.Sm) { CodeBlockContent("# Add the component\npnpm dlx @esuiss/ui add code-block", language = "bash") }
        CodeBlock(size = CodeBlockSize.Sm) { CodeBlockContent("{ \"name\": \"acme\", \"private\": true, \"version\": \"1.0.0\" }", language = "json") }
        CodeBlock(size = CodeBlockSize.Sm) {
            CodeBlockContent("struct Greeting: View {\n  let name: String\n  var body: some View { Text(\"Hello, \\(name)\") }\n}", language = "swift")
        }
        CodeBlock(size = CodeBlockSize.Sm) {
            CodeBlockContent("@Composable\nfun Greeting(name: String) {\n  Text(\"Hello, \$name\")\n}", language = "kotlin")
        }
    }
}
```

### Footer

A `CodeBlockFooter` with secondary text below the code.

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

export function CodeBlockFooterExample() {
  return (
    <CodeBlock>
      <CodeBlockHeader>
        <CodeBlockTitle>Logs</CodeBlockTitle>
        <CodeBlockActions>
          <CodeBlockCopyButton value="Compiled in 4.2s" />
        </CodeBlockActions>
      </CodeBlockHeader>
      <CodeBlockContent language="bash">
        {"$ pnpm build\nCompiled in 4.2s\nGenerated 12 static pages"}
      </CodeBlockContent>
      <CodeBlockFooter>
        <Text>Exited with code 0</Text>
      </CodeBlockFooter>
    </CodeBlock>
  )
}
```

```swift
import EsuissUI
import SwiftUI

struct CodeBlockFooterExample: View {
  var body: some View {
    CodeBlock {
      CodeBlockHeader {
        CodeBlockTitle("Logs")
        CodeBlockActions { CodeBlockCopyButton(value: "Compiled in 4.2s") }
      }
      CodeBlockContent("$ pnpm build\nCompiled in 4.2s\nGenerated 12 static pages", language: "bash")
      CodeBlockFooter { Text("Exited with code 0") }
    }
  }
}
```

```kotlin
import androidx.compose.runtime.Composable
import com.esuiss.ui.components.CodeBlock
import com.esuiss.ui.components.CodeBlockActions
import com.esuiss.ui.components.CodeBlockContent
import com.esuiss.ui.components.CodeBlockCopyButton
import com.esuiss.ui.components.CodeBlockFooter
import com.esuiss.ui.components.CodeBlockHeader
import com.esuiss.ui.components.CodeBlockTitle
import com.esuiss.ui.components.Text

@Composable
fun CodeBlockWithFooter() {
    CodeBlock {
        CodeBlockHeader {
            CodeBlockTitle("Logs")
            CodeBlockActions { CodeBlockCopyButton("Compiled in 4.2s") }
        }
        CodeBlockContent("$ pnpm build\nCompiled in 4.2s\nGenerated 12 static pages", language = "bash")
        CodeBlockFooter { Text("Exited with code 0") }
    }
}
```

## API Reference

### CodeBlock

The surface that holds the header, code and footer.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | `"default" \| "destructive"` | `"default"` | Muted surface or a destructive tint for errors. |
| `size` | `"default" \| "sm"` | `"default"` | Text size, padding and corner radius. |

Also accepts the props of `ViewProps`.

### CodeBlockHeader

A row above the code for an icon, a title and actions.

### CodeBlockTitle

One line of text that truncates, usually a file name.

### CodeBlockActions

Holds buttons at the end of the header.

### CodeBlockCopyButton

Copies `value` and shows a check for two seconds.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `string` | — | The text to copy. |

### CodeBlockContent

The code, from a string or `CodeBlockLine` children.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `language` | `string` | — | Colors the code: `ts`, `tsx`, `js`, `json`, `bash`, `python`, `swift`, `kotlin` and their aliases. |
| `wrap` | `boolean` | `false` | Wraps long lines instead of scrolling sideways. |
| `lineNumbers` | `boolean` | `false` | Shows a number before each line. |
| `startLine` | `number` | `1` | The number of the first line. |

### CodeBlockLine

One line of code.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | `"default" \| "added" \| "removed" \| "highlighted"` | `"default"` | Tints the line and adds a + or − marker for diffs. |

### CodeBlockFooter

Secondary text below the code.

## Accessibility

- The copy button has the label Copy, and Copied after a copy.
- Line numbers and diff markers are hidden from assistive technology.

