CodeBlock
A block of code to read and copy: an optional header with the file name and the language, a copy button that says when it copied, line numbers, highlighted lines, and long lines that scroll sideways or wrap. A small built-in highlighter colors TS, JS, JSON, CSS, HTML and shell with text tokens; pass highlight to plug in another one.
Preview
import { CodeBlock } from "@/components/ui/code-block"
const codeSampleTsx = "import { Button } from \"@/components/ui/button\"\n\nexport default function Page() {\n // Save the draft before leaving.\n return <Button variant=\"solid\" onClick={() => save(42)}>Save</Button>\n}"
<CodeBlock language="tsx" filename="app/page.tsx" showLineNumbers code={codeSampleTsx} />Usage
When to use
- Show code to read or copy in docs, a setup guide or a changelog.
- Point at the lines of a file that matter, with line numbers and highlighted lines.
When not to use
- A word or a short name inside a sentence. Use a code element in the text.
- Code the reader edits. Use a code editor or a Textarea.
- A long log that streams in. Use a scrolling log view.
Variants
variant
default sits sunken in a page or card, surface stands on a sunken area, plain has no fill and no edge, for code inside another panel.
import { CodeBlock } from "@/components/ui/code-block"
const codeSampleTsx = "import { Button } from \"@/components/ui/button\"\n\nexport default function Page() {\n // Save the draft before leaving.\n return <Button variant=\"solid\" onClick={() => save(42)}>Save</Button>\n}"
<CodeBlock language="tsx" filename="app/page.tsx" showLineNumbers code={codeSampleTsx} />
<CodeBlock language="tsx" filename="app/page.tsx" variant="surface" showLineNumbers code={codeSampleTsx} />
<CodeBlock language="tsx" filename="app/page.tsx" variant="plain" showLineNumbers code={codeSampleTsx} />size
Code text, line height and padding: 12, 14 or 16px text.
import { CodeBlock } from "@/components/ui/code-block"
const codeSampleTsx = "import { Button } from \"@/components/ui/button\"\n\nexport default function Page() {\n // Save the draft before leaving.\n return <Button variant=\"solid\" onClick={() => save(42)}>Save</Button>\n}"
<CodeBlock language="tsx" filename="app/page.tsx" size="sm" showLineNumbers code={codeSampleTsx} />
<CodeBlock language="tsx" filename="app/page.tsx" showLineNumbers code={codeSampleTsx} />
<CodeBlock language="tsx" filename="app/page.tsx" size="lg" showLineNumbers code={codeSampleTsx} />Examples
File
A file name and a language in the header, with the copy button at the end.
import { CodeBlock } from "@/components/ui/code-block"
const codeSampleTsx = "import { Button } from \"@/components/ui/button\"\n\nexport default function Page() {\n // Save the draft before leaving.\n return <Button variant=\"solid\" onClick={() => save(42)}>Save</Button>\n}"
<CodeBlock filename="app/page.tsx" language="tsx" code={codeSampleTsx} />Line numbers and highlighted lines
highlightLines marks lines with the info fill and an info bar; count them from startLine.
import { CodeBlock } from "@/components/ui/code-block"
const codeSampleTsx = "import { Button } from \"@/components/ui/button\"\n\nexport default function Page() {\n // Save the draft before leaving.\n return <Button variant=\"solid\" onClick={() => save(42)}>Save</Button>\n}"
<CodeBlock filename="app/page.tsx" language="tsx" code={codeSampleTsx} showLineNumbers highlightLines={[4, 5]} />Excerpt
startLine numbers an excerpt from where it sits in the file.
import { CodeBlock } from "@/components/ui/code-block"
const codeSampleCss = ":root {\n --radius-container: 12px;\n --spacing-lg: 16px;\n}"
<CodeBlock filename="tokens.css" language="css" code={codeSampleCss} showLineNumbers startLine={42} highlightLines={[43]} />Wrap
Long lines scroll sideways by default. showWrapButton adds a toggle; defaultWrap starts wrapped.
import { CodeBlock } from "@/components/ui/code-block"
const codeSampleLong = "npx shadcn@latest add https://ui.example.com/r/code-block.json https://ui.example.com/r/button.json --overwrite --yes"
<CodeBlock language="bash" code={codeSampleLong} showWrapButton />Without a header
With no file name or language the copy button floats at the top end corner.
import { CodeBlock } from "@/components/ui/code-block"
<CodeBlock language="bash" showLanguage={false} code="npx shadcn@latest add @werewolf/code-block" />Plain text
highlight={false} turns the colors off; a language with no built-in grammar renders plain too.
import { CodeBlock } from "@/components/ui/code-block"
const codeSampleText = "Werewolf UI\nTokens first, components second.\nEvery color comes from a token."
<CodeBlock filename="notes.txt" highlight={false} code={codeSampleText} />Your own highlighter
Pass a function that returns tokens per line, such as one built from a server-side highlighter's output.
import { CodeBlock } from "@/components/ui/code-block"
const codeSampleSql = "SELECT name, email\nFROM users\nWHERE active = true\nORDER BY name;"
const SQL_KEYWORDS = /\b(SELECT|FROM|WHERE|ORDER|BY|AND|OR|true|false)\b/
const highlightSql: CodeBlockHighlighter = (code) =>
code.split("\n").map((line) =>
line
.split(/(\b(?:SELECT|FROM|WHERE|ORDER|BY|AND|OR|true|false)\b)/)
.filter(Boolean)
.map((text) => ({ text, className: SQL_KEYWORDS.test(text) ? "text-text-purple" : undefined }))
)
<CodeBlock language="sql" code={codeSampleSql} highlight={highlightSql} />Tall code
maxHeight caps the code; it scrolls both ways and takes focus for the keyboard.
import { CodeBlock } from "@/components/ui/code-block"
const codeSampleJson = "{\n \"name\": \"@werewolf/code-block\",\n \"type\": \"registry:ui\",\n \"private\": false,\n \"version\": 1,\n \"dependencies\": [\"@tabler/icons-react\"],\n \"registryDependencies\": [\"@werewolf/utils\", \"@werewolf/button\"]\n}"
<CodeBlock filename="package.json" language="json" code={codeSampleJson} showLineNumbers maxHeight="160px" />Do and don't
Do
- Name the file when the code belongs in one, so the reader knows where to paste it.
- Highlight the lines that changed or that the text talks about, and say so in the text.
- Keep long lines scrolling for code people copy; offer the wrap toggle for reading on narrow screens.
Don't
- Do not put a CodeBlock inside a sentence; use a code element for a name or a short command.
- Do not rely on color to tell lines apart; highlighted lines need words around them.
- Do not add a heavy highlighter just for color; the built-in one covers the common languages, and highlight plugs in another one only where needed.
Props
| Name | Type | Values | Default | Description |
|---|---|---|---|---|
| code required | string | - | - | The code, as plain text. It is what the copy button copies. |
| language | string | - | - | The language, such as tsx, json, css, html or bash. Shown in the header, and picks the built-in grammar. |
| filename | string | - | - | A file name at the start of the header, such as app/page.tsx. It also names the code region. |
| showLanguage | boolean | - | true | Shows the language in the header. |
| variant | enum | default | surface | plain | default | default sits sunken in a page or card, surface stands on a sunken area, plain has no fill and no edge, for code inside another panel. |
| size | enum | sm | md | lg | md | Code text, line height and padding: 12, 14 or 16px text. |
| showLineNumbers | boolean | - | false | Numbers each line. |
| startLine | number | - | 1 | The number of the first line, for an excerpt. |
| highlightLines | number[] | - | - | Line numbers to mark, counted from startLine, such as [3, 4]. |
| wrap | boolean | - | - | Long lines wrap instead of scrolling sideways. Controlled; pair with onWrapChange. |
| defaultWrap | boolean | - | false | Whether long lines start wrapped, uncontrolled. |
| onWrapChange | (wrap: boolean) => void | - | - | Called when the wrap toggle is pressed. |
| showWrapButton | boolean | - | false | Shows a toggle that wraps long lines. |
| showCopyButton | boolean | - | true | Shows the copy button. |
| onCopy | (code: string) => void | - | - | Called after the code is copied. |
| highlight | boolean | CodeBlockHighlighter | - | true | true uses the built-in highlighter, false shows plain text, and a function (code, language) => tokens per line plugs in another one, such as a server-side highlighter's tokens. |
| maxHeight | string | - | - | The tallest the code grows before it scrolls, as a CSS length, such as 320px. |
| labels | { copy?: string; copied?: string; copyFailed?: string; wrap?: string; highlighted?: string; region?: string } | - | - | Button names, messages and the words read for a highlighted line and the code region. Defaults: Copy code, Copied, Couldn't copy, Wrap lines, (highlighted), Code. Vietnamese: Sao chép mã, Đã sao chép, Không sao chép được, Xuống dòng, (đánh dấu), Mã. |
Anatomy
| Part | Description |
|---|---|
| root | The figure: a sunken, surface or plain panel holding the header and the code. |
| header | The figcaption: file name and language at the start, buttons at the end, over a quiet line. |
| name | The file name, in monospace, cut with an ellipsis. |
| label | The language, such as tsx, in secondary text. |
| actions | The wrap toggle and the copy button, ghost icon Buttons in size sm. Without a header they float at the top end corner. |
| content | The pre that scrolls sideways while lines are long. It takes focus so the keyboard can scroll it. |
| line | One line of code. A highlighted line takes the info fill across the whole scroll width and an info bar at its start. |
| lineNumber | The number of the line, in secondary text, right aligned. It turns to the info text on a highlighted line. |
Accessibility
Role: figure
| Keys | Action |
|---|---|
| Tab | Moves to the wrap toggle and the copy button, then to the code, which scrolls with the arrow keys while long lines overflow. |
| Enter / Space | On the copy button, copies the code. On the wrap toggle, wraps or unwraps long lines. |
- With a file name or a language the figure is named by its header, so the code is announced as, EN `app/page.tsx tsx`.
- The copy button is named EN `Copy code`. After a copy its name turns to EN `Copied` for two seconds and a status region announces it.
- The wrap toggle is a pressed button (aria-pressed), named EN `Wrap lines`.
- Line numbers are hidden from screen readers and from text selection, so a copied selection holds only code.
- A highlighted line is shown with the info fill and an info bar at its start, and screen readers hear the word highlighted after it. Say what it means in the text around the block too; color alone does not reach every reader.
Install
npx werewolf-ui add code-blockRun npx werewolf-ui init once in your app before adding components. Init sets up tokens, their CSS import and agent rules. Add installs this component and its dependencies while preserving your theme. See Installation for requirements and the alternative registry setup.