On this page
Save.tsx
import { Button } from "@/components/Button";
export function Save() {
return <Button variant="primary">Save changes</Button>;
}CodeBlockExample.tsx
import { CodeBlock } from "@/components/CodeBlock";
const code = `import { Button } from "@/components/Button";
export function Save() {
return <Button variant="primary">Save changes</Button>;
}`;
export function CodeBlockExample() {
return (
<CodeBlock label="Save.tsx" language="tsx">
{code}
</CodeBlock>
);
}Examples
Basic
CSS
.card {
padding: var(--space-inset-md);
border-radius: var(--radius-surface);
}Long lines
Terminal
pnpm --filter web exec vitest run src/components/CodeBlock src/components/Typography src/components/types.test.tsx --reporter=verbose --coverage.enabled=falseTypeScript
import type { ComponentPropsWithRef } from "react";
export type CodeBlockProps = CodeBlockOwnProps & Omit<ComponentPropsWithRef<"div">, keyof CodeBlockOwnProps>; // every native div prop reaches the root elementLines too wide for the panel scroll inside the block. While one scrolls, it's a Tab stop:
Announced as: “Terminal, group”
Wrapped
Terminal
pnpm --filter web exec vitest run src/components/CodeBlock src/components/Typography src/components/types.test.tsx --reporter=verbose --coverage.enabled=falseTypeScript
import type { ComponentPropsWithRef } from "react";
export type CodeBlockProps = CodeBlockOwnProps & Omit<ComponentPropsWithRef<"div">, keyof CodeBlockOwnProps>; // every native div prop reaches the root elementWithout copy
CSS
.card {
padding: var(--space-inset-md);
border-radius: var(--radius-surface);
}Without line numbers
CSS
.card {
padding: var(--space-inset-md);
border-radius: var(--radius-surface);
}Customized with component tokens
CSS
.customCodeBlock {
--component-code-block-border: var(--color-accent-border);
--component-code-block-label-color: var(--color-accent-text);
}Props
Any native prop not listed here goes to the root element unchanged.
children is plain text, trimmed by one line break at each end. label holds no links or buttons. There's no syntax highlighting: language only sets the code's class. className and native props reach the root div.
| Prop | Type | Default | Description |
|---|---|---|---|
label | ReactNode | — | Required. What the code is: a language, a file name or a purpose. Shown in the header, and names the block while it scrolls. No interactive content. |
children | string | — | Required. The code, as plain text. One line break is trimmed at each end; everything else, indentation included, is kept. |
language | string | — | Sets class="language-<language>" on the code. There's no highlighting. |
wrap | boolean | false | Wraps long lines at the edge instead of scrolling, breaking inside a word only where it would overflow. A wrapped block is never a Tab stop. |
copy | boolean | true | Shows the "Copy code" button at the end of the header. |
lineNumbers | boolean | true | Numbers each line in a gutter at the start, which stays put while the code scrolls. A wrapped line keeps one number. Decorative: never copied, selected or announced. |
Component tokens
| Token | Copy | Description | Aliases | Light | Dark | Preview |
|---|---|---|---|---|---|---|
--component-code-block-background | Block fill, behind the header and the code. | color.surface.code | #edf2f8from color.gray.50 | #111827from color.slate.925 | ||
--component-code-block-text | Code text. | color.text.default | #17202bfrom color.gray.900 | #fffffffrom color.gray.0 | ||
--component-code-block-border | Line around the block and under the header (decorative: the fill and the label mark the block). | color.border.subtle | #e5dccbfrom color.sand.200 | #272f3ffrom color.slate.800 | ||
--component-code-block-border-width | Line thickness. | effect.border.width | 2pxfrom effect.line.2 | 2pxfrom effect.line.2 | ||
--component-code-block-radius | Block corner radius. | radius.control | 4pxfrom radius.4 | 4pxfrom radius.4 | ||
--component-code-block-header-padding-block | Block padding of the header; clears the copy button's focus ring. | space.stack.xs | 0.25remfrom space.1 | 0.25remfrom space.1 | ||
--component-code-block-header-padding-inline | Inline padding of the header, lining the label up with the code. | space.inset.sm | 0.5remfrom space.2 | 0.5remfrom space.2 | ||
--component-code-block-header-gap | Space between the label and the copy button. | space.inline.md | 1remfrom space.4 | 1remfrom space.4 | ||
--component-code-block-label-color | Header label text. | color.text.muted | #455464from color.gray.700 | #e6ecf3from color.slate.100 | ||
--component-code-block-failure-color | The visible "Couldn't copy" text beside the copy button (its wording states the failure, not only its color). | color.error.text | #b3261efrom color.red.700 | #f4a097from color.red.300 | ||
--component-code-block-line-number-color | Line numbers (decorative: never copied or announced, but still readable). | color.text.subtle | #5d6876from color.gray.600 | #d3dce6from color.slate.200 | ||
--component-code-block-line-number-gap | Space between the line numbers and the code. | space.inline.md | 1remfrom space.4 | 1remfrom space.4 | ||
--component-code-block-padding | Space around the code. | space.inset.sm | 0.5remfrom space.2 | 0.5remfrom space.2 | ||
--component-code-block-font-family | Code font. | type.family.code | 'Fira Code Variable', 'Fira Code Fallback', ui-monospace, SFMono-Regular, Menlo, Consolas, monospacefrom type.font-stack.mono | 'Fira Code Variable', 'Fira Code Fallback', ui-monospace, SFMono-Regular, Menlo, Consolas, monospacefrom type.font-stack.mono | ||
--component-code-block-font-size | Code text size. | type.size.small | 0.875remfrom type.font-size.sm | 0.875remfrom type.font-size.sm | ||
--component-code-block-line-height | Code line height. | type.leading.body | 1.6from type.line-height.normal | 1.6from type.line-height.normal | ||
--component-code-block-focus-ring-color | Focus indicator of an overflowing block, drawn inside it so it stays clear of the border and the header line. | color.focus.ring | #005f7bfrom color.cyan.700 | #54d8f4from color.cyan.300 |
Accessibility
- The label comes first in reading order, so screen-reader users hear what the code is before it. While the code scrolls, the label also names its focusable group.
- While its lines are too wide to fit, the code is a focusable group named by the label, so a keyboard can scroll it. Safari doesn't make scrolling boxes focusable on its own. Once it fits, it's no longer a Tab stop. Before the page's script runs, every block without wrap is one.
- It's a group, not a region, so a page of code samples doesn't fill the landmarks list.
- Use wrap for prose-like samples, where scrolling would hide the ends of sentences.
- Line numbers are decorative: they're generated by CSS with empty alternative text (content: counter(line) / ""), so they're never announced, selected or copied. Mention a line by its content, not its number, in text that relies on them.
- Copying says "Copied" through a polite status message, and the icon shows a check, for 2 seconds. If the clipboard refuses, the message says "Couldn't copy", shown beside the button as well, with a warning icon. The button's name stays "Copy code", so it isn't announced again.
- Every copy button is named "Copy code"; each follows its block's label in reading order, and its name shows as a tooltip.
- In forced colors, the border, the header line and the focus ring keep system colors.
| Key | Action |
|---|---|
| Tab / Shift+Tab | Moves to the copy button, then to the code while it's too wide to fit. |
| ← / → | Scrolls the focused code sideways. |
| Enter / Space | On the copy button, copies the code. |