# Code block

Shows code or a log in a panel, with a copy button, line numbers and highlighting you bring.

- **Import:** `import { CodeBlock } from "@booleanpress/ui/code-block"`
- **Page:** <https://ui.booleanpress.com/components/code-block> · @booleanpress/ui 0.2.0

## Usage

Pass the code as plain text in `code`. The panel copies exactly that text.

```tsx
import { CodeBlock } from "@booleanpress/ui/code-block"

export function Snippet() {
  return <CodeBlock title="mailer.ts" code={source} language="ts" lineNumbers />
}
```

`title` adds a bar above the code with the buttons in it; without one, the buttons sit in the top corner. `lineNumbers` numbers the lines in a gutter that is not selected or copied with the code. `highlightLines` marks lines by number, from 1. `maxHeight` caps the height, and the code area scrolls; `wrapToggle` adds a button that wraps long lines (or set `wrap` and `onWrapChange` yourself). `copyable={false}` removes the copy button.

The package has no highlighter. To colour the code, run Shiki or Prism yourself and pass its HTML as `html`, keeping the plain text in `code` for copying:

```tsx
const html = await codeToHtml(source, { lang: "ts", theme: "github-light" })
<CodeBlock code={source} html={html} language="ts" />
```

A highlighter's own `<pre><code>` wrapper is dropped, and an element that runs over several lines is closed and reopened on each, so line numbers and highlighted lines still work. The HTML is inserted without any cleaning, so it must be trusted: a highlighter's output is, as it escapes the code it colours, but HTML from anywhere else, above all from what a person typed, must be sanitised first. For code inside a sentence, use `InlineCode` from Typography.

## Examples

### Basic

Plain text with the copy button in the corner.

```tsx
import { CodeBlock } from "@booleanpress/ui/code-block"

const CODE = `import { createMailer } from "@example/mail"

const mailer = createMailer({
  host: "smtp.example.com",
  port: 587,
})`

export default function CodeBlockBasic() {
  return <CodeBlock code={CODE} language="ts" className="w-full max-w-lg" />
}
```

### With title

`title` adds a file-name bar that holds the buttons.

```tsx
import { CodeBlock } from "@booleanpress/ui/code-block"

const CODE = `{
  "mailer": "primary",
  "from": "hello@example.com",
  "retries": 3
}`

export default function CodeBlockWithTitle() {
  return <CodeBlock title="mailer.config.json" code={CODE} language="json" className="w-full max-w-lg" />
}
```

### Line numbers

`lineNumbers` adds a gutter that is left out of selection and copying.

```tsx
import { CodeBlock } from "@booleanpress/ui/code-block"

const CODE = `curl https://api.example.com/v1/messages \\
  -H "Authorization: Bearer $API_KEY" \\
  -H "Content-Type: application/json" \\
  -d '{"to": "ada@example.com", "subject": "Welcome"}'`

export default function CodeBlockLineNumbers() {
  return <CodeBlock title="Send a message" code={CODE} language="bash" lineNumbers className="w-full max-w-lg" />
}
```

### Highlighted lines

`highlightLines={[3, 4]}` fills those lines from edge to edge.

```tsx
import { CodeBlock } from "@booleanpress/ui/code-block"

const CODE = `const mailer = createMailer({
  host: "smtp.example.com",
  port: 587,
  secure: false,
  auth: { user: "apikey", pass: process.env.SMTP_KEY },
})`

export default function CodeBlockHighlightedLines() {
  return (
    <CodeBlock
      title="mailer.ts"
      code={CODE}
      language="ts"
      lineNumbers
      highlightLines={[3, 4]}
      className="w-full max-w-lg"
    />
  )
}
```

### Long (scrolls)

`maxHeight` makes the code area scroll, in both directions; `wrapToggle` wraps long lines.

```tsx
import { CodeBlock } from "@booleanpress/ui/code-block"

const LINES = Array.from(
  { length: 24 },
  (_, index) =>
    `2026-10-0${(index % 9) + 1}T09:${String(10 + index).padStart(2, "0")}:00Z  delivered  msg_01J9X4T2Q${index}  to=customer${index}@example.com  mailer=primary  latency=${120 + index * 7}ms`
)

export default function CodeBlockLong() {
  return (
    <CodeBlock
      title="delivery.log"
      code={LINES.join("\n")}
      language="log"
      lineNumbers
      wrapToggle
      maxHeight={240}
      className="w-full max-w-lg"
    />
  )
}
```

### Inline

`InlineCode` from Typography, for code in running text.

```tsx
import { InlineCode } from "@booleanpress/ui/typography"

export default function CodeBlockInline() {
  return (
    <p className="max-w-md text-sm/normal">
      Set <InlineCode>SMTP_KEY</InlineCode> in your environment, then call <InlineCode>createMailer()</InlineCode> with the
      host and port your provider gives you.
    </p>
  )
}
```

### Pre-highlighted HTML

`html` takes a highlighter's output; here a small hand-written one coloured with tokens.

```tsx
import { CodeBlock } from "@booleanpress/ui/code-block"

const CODE = `// Retry a failed delivery
const result = await mailer.retry("msg_01J9X4T2QZ")
console.log(result.status)`

// What a highlighter returns, written by hand here: Shiki's or Prism's HTML goes in the same prop.
const HTML = [
  `<span class="text-muted-foreground italic">// Retry a failed delivery</span>`,
  `<span class="text-destructive-strong">const</span> result = <span class="text-destructive-strong">await</span> mailer.<span class="text-info-strong">retry</span>(<span class="text-success-tag-foreground">"msg_01J9X4T2QZ"</span>)`,
  `console.<span class="text-info-strong">log</span>(result.status)`,
].join("\n")

export default function CodeBlockPreHighlighted() {
  return <CodeBlock title="retry.ts" code={CODE} html={HTML} language="ts" lineNumbers className="w-full max-w-lg" />
}
```

## Accessibility

**Semantics.** A `<figure>`, captioned by the title bar when there is one, holding a `<pre>` with `role="region"` and a `<code>`. Line numbers are hidden from assistive technology.

**Labels.** The code area is named from the provider's `codeBlock` string, "{title} code", filled with `title`, else `language`; pass `aria-label` to name it yourself. The wrap toggle is named by `wrapLines` and reports its state with `aria-pressed`; the copy button is CopyButton's.

**Focus.** The wrap toggle and copy button come first in the tab order, as they sit first on screen, then the code area, so a keyboard can scroll it with the arrow keys; the panel shows the focus outline round its edge while the code area has focus.

**Known limits.**

- Highlighted lines are marked by colour only; a screen reader does not hear which lines they are. Say it in the text around the panel.
- Colours from `html` are your highlighter's: check their contrast on `--card` in both themes.
- `html` is inserted as HTML without cleaning: pass a highlighter's output or HTML you have sanitised, never raw HTML built from what a person typed.
- Without a title the buttons sit over the code's top right-hand corner, on a right-to-left page too, as the code reads left to right; a first line that runs under them scrolls into view.

### Keyboard

| Key | Behaviour |
| --- | --- |
| Tab | Moves to the wrap toggle and the copy button, then to the code area, where the arrow keys scroll it. |
| Enter or Space | On the wrap toggle, wraps or unwraps long lines; on the copy button, copies the code. |

## API

### CodeBlock

Renders a `figure` and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `code` (required) | `string` |  | The code as plain text: what it shows (unless `html` is given) and what the copy button copies. |
| `copyable` | `boolean` | `true` | Shows the copy button. `true` by default. |
| `defaultWrap` | `boolean` | `false` | Whether long lines start wrapped, when it controls itself. |
| `highlightLines` | `number[]` |  | Line numbers, from 1, to mark with the hovered-surface fill. |
| `html` | `string` |  | The same code, highlighted, as HTML: what Shiki's `codeToHtml` or Prism's `highlight` returns. It is inserted as HTML without any cleaning, so it must be trusted: a highlighter's output is (it escapes the code it colours); HTML from anywhere else, above all from what a person typed, must be sanitised before it gets here. |
| `language` | `string` |  | The language, set as `data-language` and a `language-*` class on the `<code>`, and naming the region without a title. |
| `lineNumbers` | `boolean` | `false` | Numbers each line, in a gutter that is not selected or copied with the code. |
| `maxHeight` | `string \| number` |  | The tallest the code area grows before it scrolls: px as a number, or any CSS length. |
| `onWrapChange` | `((wrap: boolean) => void)` |  | Called with `true` or `false` when the wrap toggle is pressed. |
| `title` | `string` |  | A file name or caption, shown in a bar above the code. |
| `wrap` | `boolean` |  | Whether long lines wrap, when you control it. Pair it with `onWrapChange`. |
| `wrapToggle` | `boolean` | `false` | Shows a toggle button that wraps and unwraps long lines. |

Every part takes `className`, merged with its defaults by `cn()`, and `ref`, which reaches the element it renders.

**Data attributes:** `data-slot="code-block-actions"` (CodeBlock), and `data-wrap`, `data-language`, `data-highlighted`.

**Provider strings:** `codeBlock`, `wrapLines` (`BooleanUIProvider`'s `strings`).

## Theming

The panel is `--card` with the border token and an 8 px radius; line numbers are `--muted-foreground`, highlighted lines `--accent`. Code uses `font-mono`: set `--font-mono` in your theme for another typeface.

| Token | Used for |
| --- | --- |
| `--accent` | background |
| `--accent-foreground` | text |
| `--card` | background |
| `--card-foreground` | text |
| `--foreground` | text |
| `--muted-foreground` | text |
| `--ring` | outline |
