Skip to main content
BM

CodeBlock

A labelled, copyable code sample whose long lines scroll inside it.

View source on GitHub

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=false
TypeScript
import type { ComponentPropsWithRef } from "react";

export type CodeBlockProps = CodeBlockOwnProps & Omit<ComponentPropsWithRef<"div">, keyof CodeBlockOwnProps>; // every native div prop reaches the root element

Lines 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=false
TypeScript
import type { ComponentPropsWithRef } from "react";

export type CodeBlockProps = CodeBlockOwnProps & Omit<ComponentPropsWithRef<"div">, keyof CodeBlockOwnProps>; // every native div prop reaches the root element

Without 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.

CodeBlock props
PropTypeDefaultDescription
labelReactNode—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.
childrenstring—Required. The code, as plain text. One line break is trimmed at each end; everything else, indentation included, is kept.
languagestring—Sets class="language-<language>" on the code. There's no highlighting.
wrapbooleanfalseWraps 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.
copybooleantrueShows the "Copy code" button at the end of the header.
lineNumbersbooleantrueNumbers 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

CodeBlock component tokens
TokenCopyDescriptionAliasesLightDarkPreview
--component-code-block-backgroundBlock fill, behind the header and the code.color.surface.code#edf2f8from color.gray.50#111827from color.slate.925
--component-code-block-textCode text.color.text.default#17202bfrom color.gray.900#fffffffrom color.gray.0
--component-code-block-borderLine 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-widthLine thickness.effect.border.width2pxfrom effect.line.22pxfrom effect.line.2
--component-code-block-radiusBlock corner radius.radius.control4pxfrom radius.44pxfrom radius.4
--component-code-block-header-padding-blockBlock padding of the header; clears the copy button's focus ring.space.stack.xs0.25remfrom space.10.25remfrom space.1
--component-code-block-header-padding-inlineInline padding of the header, lining the label up with the code.space.inset.sm0.5remfrom space.20.5remfrom space.2
--component-code-block-header-gapSpace between the label and the copy button.space.inline.md1remfrom space.41remfrom space.4
--component-code-block-label-colorHeader label text.color.text.muted#455464from color.gray.700#e6ecf3from color.slate.100
--component-code-block-failure-colorThe 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-colorLine 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-gapSpace between the line numbers and the code.space.inline.md1remfrom space.41remfrom space.4
--component-code-block-paddingSpace around the code.space.inset.sm0.5remfrom space.20.5remfrom space.2
--component-code-block-font-familyCode 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-sizeCode text size.type.size.small0.875remfrom type.font-size.sm0.875remfrom type.font-size.sm
--component-code-block-line-heightCode line height.type.leading.body1.6from type.line-height.normal1.6from type.line-height.normal
--component-code-block-focus-ring-colorFocus 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.
CodeBlock keyboard
KeyAction
Tab / Shift+TabMoves to the copy button, then to the code while it's too wide to fit.
← / →Scrolls the focused code sideways.
Enter / SpaceOn the copy button, copies the code.