Skip to the content

ComponentsMisc

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"

Usage

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

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:

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.

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.

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.

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.

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.

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.

Set SMTP_KEY in your environment, then call createMailer() with the host and port your provider gives you.

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.

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

Keyboard
KeyBehaviour
TabMoves to the wrap toggle and the copy button, then to the code area, where the arrow keys scroll it.
EnterorSpaceOn 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.

CodeBlock props
PropTypeDefaultDescription
coderequiredstringThe code as plain text: what it shows (unless html is given) and what the copy button copies.
copyablebooleantrueShows the copy button. true by default.
defaultWrapbooleanfalseWhether long lines start wrapped, when it controls itself.
highlightLinesnumber[]Line numbers, from 1, to mark with the hovered-surface fill.
htmlstringThe 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.
languagestringThe language, set as data-language and a language-* class on the <code>, and naming the region without a title.
lineNumbersbooleanfalseNumbers each line, in a gutter that is not selected or copied with the code.
maxHeightstring | numberThe 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.
titlestringA file name or caption, shown in a bar above the code.
wrapbooleanWhether long lines wrap, when you control it. Pair it with onWrapChange.
wrapTogglebooleanfalseShows 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.

The tokens its classes read. Change their values in your theme, and it follows.

Theme tokens
TokenUsed for
--accentbackground
--accent-foregroundtext
--cardbackground
--card-foregroundtext
--foregroundtext
--muted-foregroundtext
--ringoutline