CodeBlock

Molecule
Syntax-highlighted code display with clipboard copy, horizontal scrolling, line emphasis, diff rendering, collapsible folding, and tabbed alternatives.

Playground

Every control sets a real prop of CodeBlockProps. The JSX below updates as you go.
Button.tsx
ts
const theme = getTheme();
setTheme(theme === "dark" ? "light" : "dark");
Props
Variant
pick one
filename
string
copy
boolean
diff
boolean
compact
boolean
wrap
boolean
JSX · CodeBlockProps
<CodeBlock filename="Button.tsx" copy />

Variants

13 examples from src/molecules/code-block/code-block.md, each rendered live from its own JSX.
tsx
export function Hello({ name }: { name: string }) {
// Greet the current user
return <Badge primary>Hi {name}</Badge>;
}
Syntax highlighting
<CodeBlock
language="tsx"
code={`export function Hello({ name }: { name: string }) {
// Greet the current user
return <Badge primary>Hi {name}</Badge>;
}`}
/>
bash
$
npm install @nannier/canvas
added 42 packages in 3s
Terminal
<CodeBlock
terminal
code={`$ npm install @nannier/canvas
added 42 packages in 3s`}
/>
ts
1
2
const theme = getTheme();
setTheme(theme === "dark" ? "light" : "dark");
Numbered
<CodeBlock
numbered
language="ts"
code={`const theme = getTheme();
setTheme(theme === "dark" ? "light" : "dark");`}
/>
npm install
Inline
<CodeBlock inline language="bash" code="npm install" />
json
{
"theme": "system",
"surface": "glass",
"primary": "#6366f1"
}
Compact
<CodeBlock
compact
language="json"
code={`{
"theme": "system",
"surface": "glass",
"primary": "#6366f1"
}`}
/>
ts
const theme = getTheme();
setTheme(theme === "dark" ? "light" : "dark");
Copy button
<CodeBlock
copy
language="ts"
code={`const theme = getTheme();
setTheme(theme === "dark" ? "light" : "dark");`}
/>
ts
const message = "This is a very long line that would normally scroll horizontally, but wrap lets it soft-wrap onto the next line instead.";
Wrap long lines
<CodeBlock
wrap
language="ts"
code={`const message = "This is a very long line that would normally scroll horizontally, but wrap lets it soft-wrap onto the next line instead.";`}
/>
deploy.sh
$
./deploy.sh --stage production
Build complete in 12s
Terminal label
<CodeBlock
terminal
filename="deploy.sh"
code={`$ ./deploy.sh --stage production
Build complete in 12s`}
/>
tsx
1
2
3
4
export function Cta() {
const label = getLabel();
return <Button primary large>{label}</Button>;
}
Highlight lines
<CodeBlock
numbered
language="tsx"
highlightLines={["2-3"]}
code={`export function Cta() {
const label = getLabel();
return <Button primary large>{label}</Button>;
}`}
/>
ts
128
129
130
function resolveTokens(scheme: Scheme) {
return scheme === "dark" ? darkTokens : lightTokens;
}
Excerpt
<CodeBlock
numbered
startLine={128}
language="ts"
code={`function resolveTokens(scheme: Scheme) {
return scheme === "dark" ? darkTokens : lightTokens;
}`}
/>
ts
-
const theme = "light";
+
const theme = getTheme();
setTheme(theme);
Diff
<CodeBlock
diff
language="ts"
code={`-const theme = "light";
+const theme = getTheme();
setTheme(theme);`}
/>
ts
export const tokens = {
primary: "#6366f1",
radius: 8,
Collapsible
<CodeBlock
collapsible
collapsedLines={3}
language="ts"
code={`export const tokens = {
primary: "#6366f1",
radius: 8,
border: "#e4e4e7",
muted: "#f4f4f5",
};`}
/>
npm install @nannier/canvas
Tabs
<CodeBlock
tabs={[
{ label: "npm", code: "npm install @nannier/canvas" },
{ label: "yarn", code: "yarn add @nannier/canvas" },
{ label: "bun", code: "bun add @nannier/canvas" },
]}
/>

Anatomy

Button.tsx
ts
1
2
const theme = getTheme();
setTheme(theme === "dark" ? "light" : "dark");
1
Frame
2
Filename
3
Code
4
Copy button
5
Line numbers
Dimmed parts appear only in some states or variants.

Props

31 props on CodeBlockProps, grouped as they are declared in source. Required props are marked *.
Prop
Type
Default
Description
Props
3 props
code
string
–
The code to render. Newlines are preserved (RN Text honors "\n").
filename
string
–
Optional filename label shown in a header bar above the surface. Honored by the plain and numbered variants (and the terminal chrome label); the inline pill has no header. When both are set, filename takes the header label and language renders as a trailing badge.
language
string
–
The snippet's language. Drives syntax highlighting (ts/tsx/js/jsx, json, bash/sh, css, html, python — anything else renders monochrome) and the header/terminal label when no filename is given.
Variant
pick one
3 props
terminal
boolean
false
numbered
boolean
false
inline
boolean
false
Density
pick one
4 props
compact
boolean
false
playground
boolean
false
The hand-off component page's JSX card (Component.dc.html lines 171 to 174, the playground's output): the block at the card corner, its header a 40 px bar on the card fill, 16 in and 8 out, with its Copy a 28 px hairline button, and the code 16 above and below, 18 in, at the default 13 / 1.7 ramp. compact wins when both are passed.
snippet
boolean
false
The hand-off component page's own snippet (Component.dc.html line 283, the Usage section's block under its paragraph): the code-card corner, with the code 16 above and below and 18 in at the default 13 / 1.7 ramp. compact and playground win over it.
guide
boolean
false
The hand-off component page's card snippet (Component.dc.html line 288, the block in a Usage guidance card): the card-snippet corner, with the code 12 above and below and 14 in at 12 / 1.65. Every other density wins over it.
Orthogonal modifiers
21 props
copy
boolean
false
Show a copy affordance (header-hosted when a header/chrome exists, else pinned top-right). On the inline variant the whole pill becomes the copy button: a raised mono chip with a trailing copy glyph that turns to a check once copied (an import line a reader takes away).
wrap
boolean
false
Soft-wrap long lines instead of scrolling them horizontally. An overflowing unwrapped block is a keyboard tab stop; arrow keys use the platform's normal scrolling.
diff
boolean
false
Render the code as a unified diff: the first column of every line is the marker ("+" added, "-" removed, anything else context). Added/removed rows tint green/red, markers stay out of the selection, and the copy chip yields the post-change code (context + added lines, markers stripped).
attached
boolean
false
Square the top edge (no top radius/border) to sit flush under an adjoining bar.
headerless
boolean
false
Draw no header bar: language still drives the highlighting, but neither it nor a filename labels the block, and copy floats over the code's top-right corner. For a snippet whose context already names it (a docs page's usage block). Plain and numbered variants; a tabs strip still needs its header, and the terminal keeps its chrome.
unbadged
boolean
false
Keep the header bar but draw no trailing language badge beside the copy chip: language still drives the highlighting, for a header whose filename already names the language (the hand-off's playground bar, Component.dc.html lines 171 to 174, is "JSX · <Interface>" and a Copy button only). Nothing changes when there is no filename, since the language is then the header's label, not a badge.
highlightLines
ReadonlyArray<number | string>
–
Emphasize lines: entries are 1-based line numbers or "start-end" ranges (e.g. [2, "5-7"]). Numbers refer to the displayed numbering, so they respect startLine. Plain and numbered variants only.
startLine
number
1
First displayed line number for the numbered variant (default 1).
collapsible
boolean
false
Fold long blocks behind a "Show all" expander (plain + numbered variants).
collapsedLines
number
8
How many lines a collapsed block shows (default 8).
expanded
boolean
false
Controlled expanded state for collapsible.
defaultExpanded
boolean
false
Uncontrolled initial expanded state (default false).
onExpandedChange
(expanded: boolean) => void
–
Called when the expander toggles (fires in both modes).
tabs
ReadonlyArray<CodeBlockTab>
–
Labeled code alternatives rendered as a tab strip in the header (or the terminal chrome) — the classic npm/yarn/bun switcher. Each tab supplies its own code and optional language/filename.
active
number
–
Controlled active tab index.
defaultActive
number
–
Uncontrolled initial tab index (default 0).
onTabChange
(index: number) => void
–
Called with the next tab index when a tab is selected (fires in both modes).
prompt
string
"$"
The terminal prompt marker (default "$"). Lines starting with it render as commands.

Accessibility and keyboard

Keyboard
Not focusable. It has no interaction of its own and takes the semantics of what it wraps.
Semantics
Roles
button
tab
tablist
States
expanded
selected
ARIA on web
aria-expanded
aria-selected
From the docs
When a line is wider than the block, Tab reaches the scrollable code and the platform's arrow keys scroll it; while it has keyboard focus, the block's card draws the theme's focus ring. Short content, wrap, and inline do not add a scrolling tab stop. Native scrolling continues to use the native ScrollView.

Platform skins

The playground's props on each platform: same tree, native shapes, type and touch sizes.
ts
export const tokens = {
primary: "#6366f1",
radius: 8,
Web
control 10 · field 10 · card 16 · Instrument Sans
ts
export const tokens = {
primary: "#6366f1",
radius: 8,
iOS
capsule controls · field 8 · card 12 · SF Pro · 44 pt
ts
export const tokens = {
primary: "#6366f1",
radius: 8,
Android
Material 3 stadium · field 4 · card 12 · Roboto · 48 dp

Usage

<CodeBlock
language="ts"
code={`const theme = getTheme();
setTheme(theme === "dark" ? "light" : "dark");`}
/>
The variant axis picks the surface (terminal > numbered > inline > plain, first match wins); the density axis is compact (the tightened ramp), playground (the hand-off component page's JSX card: the block at the card corner, its header a 40 px bar on the card fill, 16 in and 8 out, its Copy a 28 px hairline button, and the code 16 above and below and 18 in at the default ramp), snippet (the page's own snippet, the Usage section's block: the code-card corner with the code 16 above and below and 18 in at the default ramp) or guide (a card's snippet, the block in a guidance card: a 10 px corner with the code 12 above and below and 14 in at 12 / 1.65), in that order of precedence; copy, wrap, diff, collapsible, attached, headerless, and unbadged stack orthogonally. language drives the in-kit highlighter (ts/tsx/js/jsx, json, bash, css, html, python; anything else renders monochrome) and, with no filename, labels a header bar above the code; with a filename it is a trailing badge beside the copy chip instead, which unbadged drops for a filename that already names the language (the hand-off's playground bar, "JSX · ButtonProps" and a Copy button only). headerless keeps the highlighting and drops the whole bar (a tabs strip and the terminal chrome stay), so copy floats over the code's corner. Long lines scroll horizontally instead of truncating, and every run of code, highlighted tokens included, is set in the theme's mono face.
The block is the v4 hand-off's code card on every platform: the sunken well on a hairline at a 14 px corner, a 36 px header of 12 px mono labels, 13 / 1.7 code, line numbers in the subtle ink, and the hand-off's code inks (each light-scheme ink held to 4.5:1 on the well). A ts or js snippet takes the hand-off's CodeBlock renderer: keywords, strings, numbers and tag names in the accent, mint, amber and cyan. A tsx or jsx snippet takes the hand-off's component-page highlighter, which paints four things and nothing else: a Capitalised tag name in the accent (the brackets and slash neutral), an attribute name before its = in cyan, a string literal in mint and a comment in the subtle ink; a bare boolean attribute, a keyword, a number and the children text stay in the code ink, so <Button>Save changes</Button> reads as one label after one accent name. The terminal is a fixed dark window in every scheme.

Tokens

Read from the playground render. Values follow the scheme and the ion hue.
--bg2
--cyan
--fd
--fm
--ft
--ion-tx
--ln
--ln2
--sf
--tx2
--tx3

Related components