# Code Block
URL: /design/components/code-block

Highlighted code on a field panel, with a title bar and a copy button.

> For AI agents: a documentation index is available at [llms.txt](/llms.txt). Use `.md` for canonical markdown pages; `.mdx` is kept as a backwards-compatible alias on supported URL paths.

\[interactive preview component CodeBlockSpecimen omitted]

Code for CodeBlockSpecimen preview:

```tsx
"use client";

import { useState, type ReactNode } from "react";
import { CodeBlock } from "@/components/ui/code-block";

function CodeBlockSpecimen(): ReactNode {
  return (
    <CodeBlock title="debounce.ts" className="my-0 w-full">
      <pre>
        <code className="text-foreground/80 block text-[12.5px]">
          <span className="line">
            {"export function debounce(fn, wait) {"}
          </span>
          <span className="line">{"  let timer;"}</span>
          <span className="line">{"  return (...args) => {"}</span>
          <span className="line">{"    clearTimeout(timer);"}</span>
          <span className="line">
            {"    timer = setTimeout(() => fn(...args), wait);"}
          </span>
          <span className="line">{"  };"}</span>
          <span className="line">{"}"}</span>
        </code>
      </pre>
    </CodeBlock>
  );
}
```

## Installation

Copy the source into `components/ui/code-block.tsx`. Registry items that depend on it install it automatically.

- code

  "use client"; import { useRef, useState, type ComponentProps, type ReactNode } from "react"; import { CheckIcon, CopyIcon } from "lucide-react"; import { cn } from "@/lib/utils"; export interface CodeBlockProps extends Omit< ComponentProps<"figure">, "title" > { /\*\* Label in the header, usually the file name. \*/ title?: ReactNode; /\*\* Classes for the scrollable code region, e.g. a max height. \*/ viewportClassName?: string; /\*\* Text for the copy button; defaults to the rendered code's text. \*/ copyText?: string; /\*\* Called after the copy button writes the clipboard. \*/ onCopied?: () => void; /\*\* Numbers the lines when the \`pre\` does not carry \`data-line-numbers\` itself. \*/ lineNumbers?: boolean; } function CopyButton({ getText, onCopied, className, }: { getText: () => string; onCopied?: (() => void) | undefined; className?: string; }) { const \[copied, setCopied] = useState(false); return ( \<button type="button" aria-label="Copy code" onClick={async () => { try { await navigator.clipboard.writeText(getText()); } catch { return; } onCopied?.(); setCopied(true); setTimeout(() => setCopied(false), 1500); }} className={cn( "text-muted-foreground hover:text-foreground grid size-6 shrink-0 place-items-center rounded-sm transition-colors", className, )} > {copied ? ( \<CheckIcon className="size-3.5" /> ) : ( \<CopyIcon className="size-3.5" /> )} \</button> ); } /\*\* \* Chrome for highlighted code: a hairline field panel with an optional title \* header and a copy button. Children carry the \`pre\` element; tokens read \* shiki's inline colors or \`--shiki-light\`, and in dark mode fall back to \* \`--shiki-dark\`. Lines render as blocks on a \`w-max\` surface so \* highlighted rows paint past the scroll fold, and \`data-line-numbers\` on the \* \`pre\` (or the \`lineNumbers\` prop) turns on a CSS counter gutter. \*/ export function CodeBlock({ title, viewportClassName, copyText, onCopied, lineNumbers, className, children, ...props }: CodeBlockProps) { const viewportRef = useRef\<HTMLDivElement>(null); const getText = () => copyText ?? viewportRef.current?.querySelector("pre")?.textContent ?? ""; return ( \<figure className={cn( "not-prose not-fumadocs-codeblock border-foreground/10 bg-foreground/\[0.025] dark:bg-foreground/\[0.04] group/code relative my-6 flex min-w-0 flex-col overflow-hidden rounded-sm border", className, )} {...props} > {title ? ( \<figcaption className="border-foreground/10 flex h-9 shrink-0 items-center justify-between gap-2 border-b py-0 ps-3.5 pe-2"> \<span className="text-muted-foreground min-w-0 truncate font-mono text-\[11px] font-medium tracking-wide"> {title} \</span> \<CopyButton getText={getText} onCopied={onCopied} /> \</figcaption> ) : ( \<CopyButton getText={getText} onCopied={onCopied} className="bg-background/80 absolute top-2 right-2 z-10 opacity-0 backdrop-blur-sm transition-opacity group-hover/code:opacity-100 focus-visible:opacity-100 \[@media(pointer:coarse)]:opacity-100" /> )} \<div ref={viewportRef} role="region" aria-label="Code" tabIndex={0} className={cn( "focus-visible:outline-foreground/20 min-w-0 overflow-x-auto py-3.5 font-mono text-\[12.5px] leading-relaxed \[font-variant-ligatures:none] focus-visible:outline-1 focus-visible:-outline-offset-1", "\[&\_code]:bg-transparent! \[&\_pre]:w-max \[&\_pre]:min-w-full \[&\_pre]:bg-transparent! \[&\_pre]:px-3.5", "\[&\_.line]:inline-block \[&\_.line]:min-h-\[1lh] \[&\_.line]:w-full", "\[&\_code\_span]:\[color:var(--shiki-light,inherit)]", "dark:\[&\_code\_span]:\[color:var(--shiki-dark)]!", "\[&\_.highlighted]:bg-blue-500/8 \[&\_.highlighted]:shadow-\[inset\_2px\_0\_0\_#3b82f6] dark:\[&\_.highlighted]:bg-blue-500/15", "\[&\_pre\[data-line-numbers]]:\[counter-reset:line]", "\[&\_pre\[data-line-numbers]\_.line]:relative \[&\_pre\[data-line-numbers]\_.line]:pl-8 \[&\_pre\[data-line-numbers]\_.line]:\[counter-increment:line]", "\[&\_pre\[data-line-numbers]\_.line]:before:text-muted-foreground/40 \[&\_pre\[data-line-numbers]\_.line]:before:absolute \[&\_pre\[data-line-numbers]\_.line]:before:left-0 \[&\_pre\[data-line-numbers]\_.line]:before:w-5 \[&\_pre\[data-line-numbers]\_.line]:before:text-right \[&\_pre\[data-line-numbers]\_.line]:before:tabular-nums \[&\_pre\[data-line-numbers]\_.line]:before:\[content:counter(line)]", lineNumbers && "\[&\_.line]:before:text-muted-foreground/40 \[counter-reset:line] \[&\_.line]:relative \[&\_.line]:pl-8 \[&\_.line]:\[counter-increment:line] \[&\_.line]:before:absolute \[&\_.line]:before:left-0 \[&\_.line]:before:w-5 \[&\_.line]:before:text-right \[&\_.line]:before:tabular-nums \[&\_.line]:before:\[content:counter(line)]", viewportClassName, )} > {children} \</div> \</figure> ); }

* title

  components/ui/code-block.tsx

* copyText

  "use client"; import { useRef, useState, type ComponentProps, type ReactNode } from "react"; import { CheckIcon, CopyIcon } from "lucide-react"; import { cn } from "@/lib/utils"; export interface CodeBlockProps extends Omit< ComponentProps<"figure">, "title" > { /\*\* Label in the header, usually the file name. \*/ title?: ReactNode; /\*\* Classes for the scrollable code region, e.g. a max height. \*/ viewportClassName?: string; /\*\* Text for the copy button; defaults to the rendered code's text. \*/ copyText?: string; /\*\* Called after the copy button writes the clipboard. \*/ onCopied?: () => void; /\*\* Numbers the lines when the \`pre\` does not carry \`data-line-numbers\` itself. \*/ lineNumbers?: boolean; } function CopyButton({ getText, onCopied, className, }: { getText: () => string; onCopied?: (() => void) | undefined; className?: string; }) { const \[copied, setCopied] = useState(false); return ( \<button type="button" aria-label="Copy code" onClick={async () => { try { await navigator.clipboard.writeText(getText()); } catch { return; } onCopied?.(); setCopied(true); setTimeout(() => setCopied(false), 1500); }} className={cn( "text-muted-foreground hover:text-foreground grid size-6 shrink-0 place-items-center rounded-sm transition-colors", className, )} > {copied ? ( \<CheckIcon className="size-3.5" /> ) : ( \<CopyIcon className="size-3.5" /> )} \</button> ); } /\*\* \* Chrome for highlighted code: a hairline field panel with an optional title \* header and a copy button. Children carry the \`pre\` element; tokens read \* shiki's inline colors or \`--shiki-light\`, and in dark mode fall back to \* \`--shiki-dark\`. Lines render as blocks on a \`w-max\` surface so \* highlighted rows paint past the scroll fold, and \`data-line-numbers\` on the \* \`pre\` (or the \`lineNumbers\` prop) turns on a CSS counter gutter. \*/ export function CodeBlock({ title, viewportClassName, copyText, onCopied, lineNumbers, className, children, ...props }: CodeBlockProps) { const viewportRef = useRef\<HTMLDivElement>(null); const getText = () => copyText ?? viewportRef.current?.querySelector("pre")?.textContent ?? ""; return ( \<figure className={cn( "not-prose not-fumadocs-codeblock border-foreground/10 bg-foreground/\[0.025] dark:bg-foreground/\[0.04] group/code relative my-6 flex min-w-0 flex-col overflow-hidden rounded-sm border", className, )} {...props} > {title ? ( \<figcaption className="border-foreground/10 flex h-9 shrink-0 items-center justify-between gap-2 border-b py-0 ps-3.5 pe-2"> \<span className="text-muted-foreground min-w-0 truncate font-mono text-\[11px] font-medium tracking-wide"> {title} \</span> \<CopyButton getText={getText} onCopied={onCopied} /> \</figcaption> ) : ( \<CopyButton getText={getText} onCopied={onCopied} className="bg-background/80 absolute top-2 right-2 z-10 opacity-0 backdrop-blur-sm transition-opacity group-hover/code:opacity-100 focus-visible:opacity-100 \[@media(pointer:coarse)]:opacity-100" /> )} \<div ref={viewportRef} role="region" aria-label="Code" tabIndex={0} className={cn( "focus-visible:outline-foreground/20 min-w-0 overflow-x-auto py-3.5 font-mono text-\[12.5px] leading-relaxed \[font-variant-ligatures:none] focus-visible:outline-1 focus-visible:-outline-offset-1", "\[&\_code]:bg-transparent! \[&\_pre]:w-max \[&\_pre]:min-w-full \[&\_pre]:bg-transparent! \[&\_pre]:px-3.5", "\[&\_.line]:inline-block \[&\_.line]:min-h-\[1lh] \[&\_.line]:w-full", "\[&\_code\_span]:\[color:var(--shiki-light,inherit)]", "dark:\[&\_code\_span]:\[color:var(--shiki-dark)]!", "\[&\_.highlighted]:bg-blue-500/8 \[&\_.highlighted]:shadow-\[inset\_2px\_0\_0\_#3b82f6] dark:\[&\_.highlighted]:bg-blue-500/15", "\[&\_pre\[data-line-numbers]]:\[counter-reset:line]", "\[&\_pre\[data-line-numbers]\_.line]:relative \[&\_pre\[data-line-numbers]\_.line]:pl-8 \[&\_pre\[data-line-numbers]\_.line]:\[counter-increment:line]", "\[&\_pre\[data-line-numbers]\_.line]:before:text-muted-foreground/40 \[&\_pre\[data-line-numbers]\_.line]:before:absolute \[&\_pre\[data-line-numbers]\_.line]:before:left-0 \[&\_pre\[data-line-numbers]\_.line]:before:w-5 \[&\_pre\[data-line-numbers]\_.line]:before:text-right \[&\_pre\[data-line-numbers]\_.line]:before:tabular-nums \[&\_pre\[data-line-numbers]\_.line]:before:\[content:counter(line)]", lineNumbers && "\[&\_.line]:before:text-muted-foreground/40 \[counter-reset:line] \[&\_.line]:relative \[&\_.line]:pl-8 \[&\_.line]:\[counter-increment:line] \[&\_.line]:before:absolute \[&\_.line]:before:left-0 \[&\_.line]:before:w-5 \[&\_.line]:before:text-right \[&\_.line]:before:tabular-nums \[&\_.line]:before:\[content:counter(line)]", viewportClassName, )} > {children} \</div> \</figure> ); }

* viewportClassName

  max-h-\[450px]

- language

  tsx

- code

  "use client"; import { useRef, useState, type ComponentProps, type ReactNode } from "react"; import { CheckIcon, CopyIcon } from "lucide-react"; import { cn } from "@/lib/utils"; export interface CodeBlockProps extends Omit< ComponentProps<"figure">, "title" > { /\*\* Label in the header, usually the file name. \*/ title?: ReactNode; /\*\* Classes for the scrollable code region, e.g. a max height. \*/ viewportClassName?: string; /\*\* Text for the copy button; defaults to the rendered code's text. \*/ copyText?: string; /\*\* Called after the copy button writes the clipboard. \*/ onCopied?: () => void; /\*\* Numbers the lines when the \`pre\` does not carry \`data-line-numbers\` itself. \*/ lineNumbers?: boolean; } function CopyButton({ getText, onCopied, className, }: { getText: () => string; onCopied?: (() => void) | undefined; className?: string; }) { const \[copied, setCopied] = useState(false); return ( \<button type="button" aria-label="Copy code" onClick={async () => { try { await navigator.clipboard.writeText(getText()); } catch { return; } onCopied?.(); setCopied(true); setTimeout(() => setCopied(false), 1500); }} className={cn( "text-muted-foreground hover:text-foreground grid size-6 shrink-0 place-items-center rounded-sm transition-colors", className, )} > {copied ? ( \<CheckIcon className="size-3.5" /> ) : ( \<CopyIcon className="size-3.5" /> )} \</button> ); } /\*\* \* Chrome for highlighted code: a hairline field panel with an optional title \* header and a copy button. Children carry the \`pre\` element; tokens read \* shiki's inline colors or \`--shiki-light\`, and in dark mode fall back to \* \`--shiki-dark\`. Lines render as blocks on a \`w-max\` surface so \* highlighted rows paint past the scroll fold, and \`data-line-numbers\` on the \* \`pre\` (or the \`lineNumbers\` prop) turns on a CSS counter gutter. \*/ export function CodeBlock({ title, viewportClassName, copyText, onCopied, lineNumbers, className, children, ...props }: CodeBlockProps) { const viewportRef = useRef\<HTMLDivElement>(null); const getText = () => copyText ?? viewportRef.current?.querySelector("pre")?.textContent ?? ""; return ( \<figure className={cn( "not-prose not-fumadocs-codeblock border-foreground/10 bg-foreground/\[0.025] dark:bg-foreground/\[0.04] group/code relative my-6 flex min-w-0 flex-col overflow-hidden rounded-sm border", className, )} {...props} > {title ? ( \<figcaption className="border-foreground/10 flex h-9 shrink-0 items-center justify-between gap-2 border-b py-0 ps-3.5 pe-2"> \<span className="text-muted-foreground min-w-0 truncate font-mono text-\[11px] font-medium tracking-wide"> {title} \</span> \<CopyButton getText={getText} onCopied={onCopied} /> \</figcaption> ) : ( \<CopyButton getText={getText} onCopied={onCopied} className="bg-background/80 absolute top-2 right-2 z-10 opacity-0 backdrop-blur-sm transition-opacity group-hover/code:opacity-100 focus-visible:opacity-100 \[@media(pointer:coarse)]:opacity-100" /> )} \<div ref={viewportRef} role="region" aria-label="Code" tabIndex={0} className={cn( "focus-visible:outline-foreground/20 min-w-0 overflow-x-auto py-3.5 font-mono text-\[12.5px] leading-relaxed \[font-variant-ligatures:none] focus-visible:outline-1 focus-visible:-outline-offset-1", "\[&\_code]:bg-transparent! \[&\_pre]:w-max \[&\_pre]:min-w-full \[&\_pre]:bg-transparent! \[&\_pre]:px-3.5", "\[&\_.line]:inline-block \[&\_.line]:min-h-\[1lh] \[&\_.line]:w-full", "\[&\_code\_span]:\[color:var(--shiki-light,inherit)]", "dark:\[&\_code\_span]:\[color:var(--shiki-dark)]!", "\[&\_.highlighted]:bg-blue-500/8 \[&\_.highlighted]:shadow-\[inset\_2px\_0\_0\_#3b82f6] dark:\[&\_.highlighted]:bg-blue-500/15", "\[&\_pre\[data-line-numbers]]:\[counter-reset:line]", "\[&\_pre\[data-line-numbers]\_.line]:relative \[&\_pre\[data-line-numbers]\_.line]:pl-8 \[&\_pre\[data-line-numbers]\_.line]:\[counter-increment:line]", "\[&\_pre\[data-line-numbers]\_.line]:before:text-muted-foreground/40 \[&\_pre\[data-line-numbers]\_.line]:before:absolute \[&\_pre\[data-line-numbers]\_.line]:before:left-0 \[&\_pre\[data-line-numbers]\_.line]:before:w-5 \[&\_pre\[data-line-numbers]\_.line]:before:text-right \[&\_pre\[data-line-numbers]\_.line]:before:tabular-nums \[&\_pre\[data-line-numbers]\_.line]:before:\[content:counter(line)]", lineNumbers && "\[&\_.line]:before:text-muted-foreground/40 \[counter-reset:line] \[&\_.line]:relative \[&\_.line]:pl-8 \[&\_.line]:\[counter-increment:line] \[&\_.line]:before:absolute \[&\_.line]:before:left-0 \[&\_.line]:before:w-5 \[&\_.line]:before:text-right \[&\_.line]:before:tabular-nums \[&\_.line]:before:\[content:counter(line)]", viewportClassName, )} > {children} \</div> \</figure> ); }

## Usage

The component is the chrome; pass it a `pre` element, highlighted or plain. In MDX every fenced code block on this site renders through it.

```
import { CodeBlock } from "@/components/ui/code-block";

export function Example() {
  return (
    <CodeBlock title="example.ts">
      <pre>
        <code>{'const answer = 42;'}</code>
      </pre>
    </CodeBlock>
  );
}
```

## Examples

### Without a title

When there is no header, the copy button floats in the corner and appears on hover.

\[interactive preview component CodeBlockBareSpecimen omitted]

Code for CodeBlockBareSpecimen preview:

```tsx
"use client";

import { useState, type ReactNode } from "react";
import { CodeBlock } from "@/components/ui/code-block";

function CodeBlockBareSpecimen(): ReactNode {
  return (
    <CodeBlock className="my-0 w-full">
      <pre>
        <code className="text-foreground/80 block text-[12.5px]">
          <span className="line">{"npx assistant-ui init"}</span>
        </code>
      </pre>
    </CodeBlock>
  );
}
```

## API Reference

### CodeBlock

- `title?`: `ReactNode` — Header label, usually the file name. Without it the copy button floats in the corner.
- `viewportClassName?`: `string` — Classes for the scrollable code region, e.g. a max height.
- `copyText?`: `string` — Text for the copy button; defaults to the rendered code's text.
- `className?`: `string` — Additional CSS classes on the outer figure.

Lines marked `.highlighted` by the highlighter get the blue gutter bar, and `data-line-numbers` on the `pre` turns on the counter gutter. In dark mode each token falls back to its `--shiki-dark` variable.