A block of stored source you can read, scroll, and copy: a bordered frame with an optional header (title, subtitle, extra actions, a copy button) over a scrollable <pre>; with none of the header slots set, only the copy button shows, floating over the body. Built for one data shape, JSON, with a hand-rolled per-line colorizer rather than a syntax-highlighting dependency; a raw <pre> reads unreadable at size, and this exists so a stored document never has to fall back to that.
The canon behind it. docs/design-language/01-foundations.md §A5 (01-foundations.md:27, mono is for identifiers and code, never decoration) and docs/design-language/10-overlays.md §A12 (10-overlays.md:56, copy affordances attach to the data plane, never the control plane, which is why copy always writes the raw source).
Header alignment. The header row centres its title block and its actions vertically (doc 59 S7), so a one-line title and the copy button share a baseline.
When to reach for it#
Reach for it anywhere a stored JSON (or plain-text) document needs to be shown read-only: an API response, a raw ingested record, a config dump. Both shipped call sites are exactly this, a raw API response and a raw catalog item, each rendered inside a modal-or-panel detail view. No speedway usage found.
Contract#
code: the source, shown exactly as given.CodeBlocknever reformats it, and copy always writes this string verbatim, never the colorized markup.language:"json" | "jsonc" | "text" | "tsx" | "ts" | "jsx" | "bash" | "python" | "yaml" | "html" | "csv", default"json"."json"runs the line-by-line colorizer below;"jsonc"is JSON plus//line comments, a comment line colours whole and everything else still runs the JSON lexer;"text"renders the lines unmodified."tsx","ts"and"jsx"share one hand-rolled whole-document lexer (colorize-tsx.tsx, exported ascolorizeSource),"ts"and"jsx"are aliases of the same config, no separate parsing per language."bash","python","yaml"and"html"run the same lexer under their own config: comment syntax, string quotes, a keyword set where the language has one,$VAR/${...}as a variable class for bash, and a barekey:at the start of a line as a key class for yaml."bash"also reads command lines: the first word of a line, or after|,;,&&,(, is the command (bold blue); the next bare word after a known multi-command tool (git, pnpm, docker, gh, kubectl, and friends) is its subcommand (violet);-fand--flagtokens are flags (amber);NAME=valuecolours the name as a variable; a\line continuation keeps the line, a>&redirection does not start a new command (colorize-tsx.tsx,shellMode)."csv"runs a column-aware colorizer, not the token lexer: the header row reads as keywords, and each data column alternates the key and string colours so columns read as columns. Same no-dependency stance, same palette tokens throughout; JSX and HTML text children are lexed as code, so a keyword inside prose colours too.title/subtitle: header text, throughrenderNode(titlein mono,subtitlemuted); JSX passes through.actions: extra header controls, placed before the copy button.size:"sm" | "md", default"sm"."sm"is the original compact 11.5px body text;"md"renders at 13px for a page-scale block (a landing card, a doc) that needs to read at a glance rather than pack density.copy: the copy button, on by default, an internal<CopyButton value={code} size="xs" variant="ghost" circle />so it always copies the raw source, not the colorized text. It sits in the header when there is one; when notitle,subtitle, oractionsis given the header bar is not rendered at all and the button floats over the body's top-right corner, so an untitled fence never carries an empty bar (owner ruling 2026-08-16).lineNumbers: a 1-based gutter, its width sized to the document's own line count.maxHeight: default"24rem". Caps the body and scrolls it inside itself, so a large document cannot push the page's own chrome off-screen.skeleton: a block-shaped placeholder instead of content.- Slot classnames per
CodeBlockClassNames:""the frame,header,title,subtitle,actions,bodythe scroll area,codethe<pre>.
The JSON colorizer#
One regex-driven lexer (JSON_TOKEN, code-block.tsx:15-16) tries the key rule before the string rule, since a key is just a string that a colon follows; tried in the other order, every key would color as a value. It runs per line rather than over the whole document, which works because pretty-printed JSON never carries a raw newline inside a token, a string's own newline is already escaped to \n (code-block.tsx:38-41). Each lexical class gets one theme color (key, string, number, boolean, nullish, punctuation), each measured to clear WCAG AA against the block's surface (code-block.tsx:18-33).
Sanctioned combinations#
| Combination | Produces | Where used | Why |
|---|---|---|---|
language="json" lineNumbers maxHeight="60vh", no header props | A tall, numbered JSON viewer for a full API response | walmart-mvp/frontend/src/pages/WalmartExplorer.tsx:287-292 | The response can be long; numbered lines make pointing at a specific line possible |
language="json" maxHeight="24rem", no header props | A compact, unnumbered JSON viewer nested inside an already-scrolling detail panel | walmart-mvp/frontend/src/pages/WalmartFeedStatus.tsx:364 | The default maxHeight is enough at this size; the surrounding panel already labels the field, so no header is needed |
Neither shipped call site sets title, subtitle, actions, copy={false}, or language="text"; both leave copy at its default true. Treat the header slots and the "text" language as implemented and correct, not as proven in production use.
Banned combinations#
Do not fall back to a raw <pre> for a stored JSON or text document. A raw <pre> reads unreadable at size, and this component exists so a stored document never has to (packages/ui/docs/code-block.md:7); a document that is only ever shown read-only belongs in CodeBlock. The likely wrong answers are a hand-styled <pre className="bg-base-200 rounded p-4"> and daisyUI's mockup-code; both lose the copy affordance, the height cap, and the colouring in one move.
Do not pre-format code or hand it markup. CodeBlock renders exactly what it is given and copy writes that string verbatim, never the colorized output (code-block.tsx:165); pass the raw source and pick the language that colours it.
Do not lift the maxHeight cap on a page-level mount. The default "24rem" scrolls a long document inside its own frame so it cannot push the page's chrome off-screen; the tall variant (maxHeight="60vh", WalmartExplorer.tsx:287-292) is still a cap. An uncapped block on a page-level surface is a layout defect, not a viewer.
Do not use CodeBlock as an editor. It has no editing affordance and no onChange; a source that must be edited is an Input / textarea concern with a CodeBlock preview beside it, not a CodeBlock with contentEditable.
Before you adopt this#
Five questions to answer before reaching for CodeBlock.
- Does the shell, a parent layout, or a global provider already render this? Not applicable, CodeBlock is page content; no shell already renders it.
- Does this app already ship a local implementation of the same thing? A raw
<pre>fallback is the local-clone trap this component replaces. - Does this app's kit pin reach the version this component or prop landed in? Not applicable, no version-landed CodeBlock prop is noted in this doc.
- Does the component derive its own accessible name and keyboard path, or must the call site supply them? Not applicable, only the header's
CopyButtonis interactive and names itself. - Which canon §A rules bind this surface, and which does the composition break? §A5 keeps mono for code; §A12 makes copy write the raw source only.
Travels with#
CopyButton, internally, for the header's copy control.renderNode, internally, fortitle/subtitle.
Snippet#
// walmart-mvp/frontend/src/pages/WalmartExplorer.tsx:287-292<CodeBlock code={JSON.stringify(result.response, null, 2)} language="json" lineNumbers maxHeight="60vh"/>