# Prose Code Block

> Code in a document, framed like the docs' code blocks, with its language and a copy button.

Source: https://ai.honestui.com/docs/components/prose-code-block

```tsx
"use client"

import { ProseCodeBlock } from "@/components/ai/prose-code-block"
import { SyntaxLine, useSyntax } from "@/lib/prose-syntax-react"

const sampleCode = `function exportTotal(rows: Row[]) {
  // Sum first, then round once.
  const total = rows.reduce((sum, row) => sum + row.amount, 0)
  return roundTotal(total)
}`

export function ProseCodeBlockDemo() {
  const syntax = useSyntax(sampleCode, "ts")
  return (
    <div className="w-full max-w-lg min-w-0">
      <ProseCodeBlock
        label="export.ts"
        code={sampleCode}
        codeClassName="honest-syntax"
        renderLine={syntax ? (line, index) => (syntax.lines[index] ? <SyntaxLine tokens={syntax.lines[index]} /> : line) : undefined}
      />
    </div>
  )
}

```

## Overview [#overview]

Prose Code Block shows code that belongs in a document: a fenced block in [Markdown](/docs/components/markdown), or a code block a model generated in a chat reply. A shell holds the language and Copy; the code sits in a panel inside it.

It does not highlight syntax itself. Pass `renderLine` and `codeClassName="honest-syntax"` to color it with [Honest Prose Syntax](/docs/prose-syntax), as Markdown does. For a file an agent wrote or changed, with line numbers or a diff, use [Code View](/docs/components/code-view).

## Anatomy [#anatomy]

<Anatomy title="ProseCodeBlock">
  <AnatomyItem name="Shell" description="The frame, with the language or file name, your actions, and Copy" />

  <AnatomyItem name="Panel" description="The code, in a scrolling block with a hairline edge" />
</Anatomy>

Installing `prose-code-block` brings [Button](https://www.honestui.com/docs/components/button) along as a registry dependency.

## Behavior [#behavior]

### Copy [#copy]

Copy puts the code on the clipboard and shows a check for a moment. If the browser refuses clipboard access, the block says "Copy failed" instead of pretending it worked. Copy is always visible, so it works with touch and keyboard as well as a pointer.

### Actions [#actions]

`actions` puts your own controls in the shell, before Copy. Pass a button for each action your application handles, such as downloading the file.

### Long lines [#long-lines]

Lines do not wrap. The panel scrolls sideways.

## Accessibility [#accessibility]

The block is a figure with the language or file name as its caption. The panel can take focus, so a keyboard user can scroll long lines, and it is named by the same label. Copy is an icon button named "Copy code", and it announces "Copied" or "Copy failed" through a polite status.

| Key                                  | Action                                         |
| ------------------------------------ | ---------------------------------------------- |
| <kbd>Tab</kbd>                       | Move to your actions, then Copy, then the code |
| Arrow keys                           | Scroll the focused code                        |
| <kbd>Enter</kbd> or <kbd>Space</kbd> | Copy                                           |

## Installation [#installation]


  

  
    <CliBlock commands="[&#x22;@honestai/prose-code-block&#x22;]" />
  

  
    
      
        Add the components it is built from.
      

      <CliBlock commands="[&#x22;button&#x22;]" />

      
        Copy and paste this file into your project.
      

      ### components/ai/prose-code-block.tsx

```tsx
"use client"

import * as React from "react"
import { Check as CheckIcon, Copy as CopyIcon } from "honestui/icons"

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

export type ProseCodeBlockLabels = {
  copy: string
  copied: string
  copyFailed: string
}

export const defaultProseCodeBlockLabels: ProseCodeBlockLabels = {
  copy: "Copy code",
  copied: "Copied",
  copyFailed: "Copy failed",
}

const COPIED_MILLISECONDS = 1500

export type ProseCodeBlockProps = Omit<React.ComponentProps<"figure">, "children" | "onCopy"> & {
  /** The language or file name, shown in the frame and used to name the code for assistive technology. */
  label: string
  code: string
  /** Colors one line. Without it, lines are plain text. */
  renderLine?: (text: string, index: number) => React.ReactNode
  /** Classes for the code surface, such as `honest-syntax`. */
  codeClassName?: string
  /** Attributes for the code surface, such as `data-syntax-palette`. */
  codeProps?: Omit<React.ComponentProps<"pre">, "children" | "className"> & { [attribute: `data-${string}`]: string | undefined }
  /** Controls placed before Copy. */
  actions?: React.ReactNode
  /** Runs after the text reaches the clipboard. */
  onCopy?: (text: string) => void
  labels?: Partial<ProseCodeBlockLabels>
}

/**
 * Code in a document, framed like the docs' code blocks: a shell with the
 * language and Copy, around a panel with the code. It does not highlight
 * syntax itself; pass `renderLine` and `codeClassName`.
 */
function ProseCodeBlock({
  label,
  code,
  renderLine,
  codeClassName,
  codeProps,
  actions,
  onCopy,
  labels: labelsProp,
  className,
  ...props
}: ProseCodeBlockProps) {
  const labels = { ...defaultProseCodeBlockLabels, ...labelsProp }
  const [copyState, setCopyState] = React.useState<"idle" | "copied" | "failed">("idle")
  const resetTimer = React.useRef<number | undefined>(undefined)
  React.useEffect(() => () => window.clearTimeout(resetTimer.current), [])

  const copy = async () => {
    try {
      await navigator.clipboard.writeText(code)
      setCopyState("copied")
      onCopy?.(code)
    } catch {
      setCopyState("failed")
    }
    window.clearTimeout(resetTimer.current)
    resetTimer.current = window.setTimeout(() => setCopyState("idle"), COPIED_MILLISECONDS)
  }

  const lines = code.split("\n")
  return (
    <figure
      data-slot="prose-code-block"
      // `not-prose` keeps a prose around this figure from restyling its code.
      className={cn("not-prose m-0 w-full min-w-0 rounded-[8px] bg-[var(--hui-color-background-neutral-primary)] p-1 text-[var(--hui-color-foreground-base-primary)]", className)}
      {...props}
    >
      <figcaption className="flex min-w-0 items-center gap-[var(--hui-space-2)] py-0.5 ps-2.5 pe-0.5">
        <span className="min-w-0 truncate font-mono text-xs text-[var(--hui-color-foreground-base-secondary)]">{label}</span>
        <div className="ms-auto flex shrink-0 items-center gap-[var(--hui-space-2)]">
          {actions}
          <Button type="button" variant="ghost" size="icon" className="h-6 w-6 rounded" aria-label={labels.copy} onClick={() => void copy()}>
            {copyState === "copied" ? <CheckIcon aria-hidden="true" /> : <CopyIcon aria-hidden="true" />}
          </Button>
        </div>
        <span role="status" className="sr-only">
          {copyState === "copied" ? labels.copied : copyState === "failed" ? labels.copyFailed : ""}
        </span>
      </figcaption>
      {/* Focusable so a keyboard user can scroll long lines. The shell is the frame, so the panel keeps only its hairline. */}
      <pre
        tabIndex={0}
        aria-label={label}
        {...codeProps}
        className={cn(
          "m-0 mt-1 overflow-x-auto rounded-[5px] bg-[var(--hui-color-background-base-primary)] px-4 py-3.5 font-mono text-[0.8125rem] leading-[1.65] shadow-[inset_0_0_0_0.5px_var(--hui-color-border-base-primary)] outline-none focus-visible:[outline:var(--hui-focus-ring)] focus-visible:outline-offset-[var(--hui-focus-ring-offset-inset)]",
          codeClassName,
          "[&&]:border-0! [&&]:rounded-[5px]!"
        )}
      >
        <code className="block w-max min-w-full">
          {lines.map((line, index) => (
            <React.Fragment key={index}>
              {renderLine ? renderLine(line, index) : line}
              {index < lines.length - 1 ? "\n" : null}
            </React.Fragment>
          ))}
        </code>
      </pre>
      {copyState === "failed" && (
        <p className="m-0 px-2.5 pt-1.5 pb-0.5 text-xs">{labels.copyFailed}</p>
      )}
    </figure>
  )
}

export { ProseCodeBlock }

```

      
        Update the import paths to match your project setup.
      
    
  


## Usage [#usage]

```tsx
import { ProseCodeBlock } from "@/components/ai/prose-code-block"
import { SyntaxLine, useSyntax } from "@/lib/prose-syntax-react"

const syntax = useSyntax(code, "ts")

<ProseCodeBlock
  label="export.ts"
  code={code}
  codeClassName="honest-syntax"
  renderLine={(line, index) => (syntax?.lines[index] ? <SyntaxLine tokens={syntax.lines[index]} /> : line)}
/>
```

## API reference [#api-reference]

### ProseCodeBlock [#prosecodeblock]

Accepts native figure props beyond those listed.

| Prop            | Type                                         | Default  | Description                                                        |
| --------------- | -------------------------------------------- | -------- | ------------------------------------------------------------------ |
| `label`         | string                                       | Required | The language or file name, shown in the shell and naming the code. |
| `code`          | string                                       | Required | The source to show and copy.                                       |
| `renderLine`    | `(text: string, index: number) => ReactNode` | None     | Colors one line. `index` counts lines from zero.                   |
| `codeClassName` | string                                       | None     | Classes for the code panel, such as `honest-syntax`.               |
| `codeProps`     | `ComponentProps<"pre">`                      | None     | Attributes for the code panel, such as `data-syntax-palette`.      |
| `actions`       | ReactNode                                    | None     | Controls for the shell, placed before Copy.                        |
| `onCopy`        | `(text: string) => void`                     | None     | Runs after the text reaches the clipboard.                         |
| `labels`        | `Partial<ProseCodeBlockLabels>`              | English  | The Copy button's name and its status messages.                    |
