Skip to content

Code Snippet

Read-only code with a copy affordance that confirms it copied. Inline, single-line and multi-line — plus the copy button everywhere else a value has to be transcribed.

Also called Copy to Clipboard, Copy Button, Code Block, Pre — in this system all of them are Code Snippet.

Live preview

Everything below is the real component. Change the controls, tab through it, and turn on Inspector Mode to read any value off the screen.

Playground
tsx
1import { createClient } from '@acme/sdk'23const client = createClient({4  apiKey: process.env.ACME_API_KEY,5  region: 'eu-west-2',6})78const deployment = await client.deployments.create({9  project: 'api-gateway',10  ref: 'main',11})

The three containers

Inline for a term in a sentence, single-line for a value to take away, multi-line for something to run. All three copy the exact characters and nothing else.

Set ACME_API_KEY in your environment, then run acme deploy from the project root.

npx @acme/cli deploy --project api-gateway
bash
curl -X POST https://api.acme.dev/v1/deployments \
  -H "Authorization: Bearer $ACME_API_KEY" \
  -d '{"project":"api-gateway","ref":"main"}'

Secrets

Masked by default, revealable, and copyable without revealing. Forcing a reveal before copy shows the key to everyone behind the user for no security benefit at all.

Live API key
•••••••••••••••••••••••••••••••••

Shown once at creation. Copy works while masked.

The prompt must not be copied

A leading $ tells the reader this is a shell command. Pasting it into a shell produces "command not found:

quot;. Render it as an aria-hidden, unselectable prefix.

RightPrefix is decoration
npm install @acme/sdk
Wrong$ is part of the string
$ npm install @acme/sdk

Copy outside a code block

The same affordance, the same confirmation. A support reference, a share link, a resource ID — all values a user would otherwise transcribe.

Support referenceERR-4021-A7F3
Share linkhttps://acme.dev/d/9fJk2Lm
Deployment IDdpl_7Hq3nR8vTx

Interaction states

Every state a user can put this component into, rendered side by side. If a state is missing here, it is missing in production too.

--dry-run
Inline
Idle copy
Copied
Focus
acme deploy
Single line
••••••••••••••••••
Masked
bash
Terminal
npx @acme/cli deploy --project api-gateway --ref main
Overflowing

Anatomy

Every part, every measurement, and the reason it is that number.

tsx
1const client = createClient({2  region: 'eu-west-2',3})

A multi-line block: language tag, line numbers, syntax colours, and the copy control in the corner nearest the reader’s exit.

  1. Type13px / 1.55 monospace

    One step below body copy — monospace runs optically larger at the same nominal size. The 1.55 leading is looser than prose because code is scanned vertically as columns.

  2. Surface--ds-surface-inset

    A well, not a raised card. Code is content set into the page, and the inset reads as "this is a different kind of text" without needing a heavy border.

  3. Padding14px, 12px with numbers

    Reduced on the left when line numbers are present, since the gutter already provides the inset.

  4. Line numbers11px, disabled tone, user-select: none

    Deliberately unselectable. Numbers landing on the clipboard is the single most common bug in this component.

  5. Copy control28px, top-right, 8px inset

    Top-right because that is where the eye leaves a left-aligned block. Always visible on touch; may fade in on hover on fine pointers, never on touch where there is no hover.

  6. ConfirmationTick for 1.6s + aria-live

    Long enough to be noticed after the eye returns from the paste target, short enough that a second copy still reads as a new event.

  7. Max height24rem, then scrolls

    About 24 lines. Past that the block dominates the page and the reader has lost the context it was illustrating.

Design tokens used

Values are read live from the running stylesheet, so this table can never drift from the code. Click any value to copy it.

Color

TokenValueUsed for
--ds-surface-inset—Block and inline background
--ds-border-subtle—Block edge and inline outline
--ds-fg—Default code text
--ds-fg-muted—Comments, and the idle copy glyph
--ds-fg-disabled—Line-number gutter and the shell prompt — both non-content
--ds-success-text—The copied confirmation
--ds-accent-text—Keywords in the highlight theme

Spacing

TokenValueUsed for
--space-3Block padding
--space-2Copy-control inset from the corner

Radius

TokenValueUsed for
--radius-lgBlock corners
--radius-xsInline snippet corners

Typography

TokenValueUsed for
font-monoAll code, at every size

Motion

TokenValueUsed for
confirm holdHow long the tick stays

Recommended sizes

Pick a size from this table. Do not invent a new one — a fourth height is how a design system starts dying.

SizeHeightPaddingRadiusTypeMin widthTouch targetWhen to use
InlineLine height2px 6px4px0.9em——Inside a sentence. Sized relative to the surrounding text so it never breaks the line rhythm.
Single line36px0 4px 0 10px8px13px——A value to take away: a command, a key, an ID. Scrolls horizontally rather than wrapping.
Multi-lineMax 24rem, then scrolls14px12px13px——Something to run or paste. About 24 lines before the block starts dominating the page.
Copy control28px————44px on coarse pointersFixed size at every container size — it is a target, not a decoration.
Gutter———11px2.5rem—Right-aligned numbers, unselectable, never part of the copied string.

Do

copy(props.code)
✗ copy(el.innerText)
Copy from the source, never from the DOMReading innerText drags in line numbers, the shell prompt, and whatever the highlighter injected. The user pastes something that will not run and has no idea why.
role="status" → “Copied”
Confirm in two channelsThe tick is for the eye; the aria-live announcement is for everyone else. A silent success is indistinguishable from a failure, and users press again until they paste to check.
••••••••••••••••••••••
Let secrets be copied while maskedRevealing a key to copy it shows it to the room and to any screen share. The clipboard does not need the pixels.
dpl_7Hq3nR8vTx
Keep the copy control visible on touchA control that appears on hover does not exist on a phone, which is exactly where selecting text by hand is hardest.

Don't

$ npm install @acme/sdk → clipboard
Do not include the prompt in the copied stringPasting "$ npm install" into a shell fails with "command not found:
quot;. The prompt is a hint that this is a command, and hints are aria-hidden decoration.

Our revolutionary new platform is blazing fast.

Do not use monospace for emphasisMonospace is a promise that these are the literal characters to type. Using it to make a word look technical trains readers to ignore that promise.
npx @acme/cli deploy --project api-gateway --ref main --region eu-west-2
Do not wrap long command linesA wrapped shell command hides where the real line breaks are, and a copied fragment with an invented newline is a command that fails in a confusing way. Scroll instead.
line 1 of 300… line 2 of 300… line 3 of 300… line 4 of 300… line 5 of 300… line 6 of 300… line 7 of 300… line 8 of 300… line 9 of 300… line 10 of 300… line 11 of 300… line 12 of 300…
Do not paste 300 lines into a panelPast about 24 lines the block stops illustrating and starts being the page. Show the ten lines that matter and link to the file.

Accessibility

Not a checklist to run at the end. These are the requirements the component was built from.

1.4.3Contrast (Minimum)AA1.4.12Text SpacingAA2.1.1KeyboardA4.1.3Status MessagesAA

Contrast

  • Every syntax colour must reach 4.5:1 against the inset surface, in both themes. Highlight themes ported from an editor almost never do — comments are the usual failure.
  • Comments are the one token allowed to sit at the bottom of the range, and they still owe 4.5:1. They are content, not chrome.
  • The line-number gutter is non-content and may use the disabled tone. Nothing a user needs to read may.
  • The copied tick must not be the only signal of success — colour alone fails for the 8% who cannot distinguish it from the idle glyph.

Keyboard

TabReaches the copy control. It is a real button, never a click handler on a div.
Enter / SpaceCopies and fires the confirmation.
TabReaches a scrollable block itself, which must carry tabindex="0" so a keyboard user can scroll it.
← / → / Home / EndScrolls a focused overflowing block horizontally.
⌘A / ⌃AInside a focused block, selects the code — and must select the code only, not the gutter.

Screen readers

  • Announce the language and line count before the code: "TypeScript, 9 lines". Reading nine lines of punctuation with no warning is disorienting.
  • The copy confirmation must be announced. "Copied" from a polite live region is enough; do not use assertive, which interrupts.
  • A masked secret should announce as masked — "API key, hidden" — so a screen-reader user knows the reveal control exists and why.

Focus & touch

  • The copy control keeps its focus ring after activation — a control that visually changes on success must not also appear to lose focus. Focus never moves on copy; the user stays exactly where they were.
  • The copy control is 44px on coarse pointers and always visible — hover-revealed controls do not exist on touch, and hand-selecting a 40-character key on a phone is the worst interaction in any developer product. Blocks scroll horizontally with momentum rather than wrapping, and the block itself must not swallow the page’s vertical scroll.
AttributeApplied toNotes
role="status"A visually hidden live regionAnnounces "Copied". Without it the confirmation is purely visual and the interaction has no feedback at all for a screen-reader user.
aria-labelThe copy buttonNames the value: "Copy API key", not "Copy". A page with six copy buttons otherwise has six identical controls.
tabindex="0"A scrollable blockRequired by 2.1.1 — a region that scrolls must be reachable by keyboard.
aria-labelThe scrollable blockNames what the code is, so the region announces as "Deployment example, code" rather than as an anonymous scroll area.
user-select: noneThe gutter and the promptNot ARIA, but the same intent: these characters are not content and must never reach a selection or the clipboard.

Code

Example usage

tsx
1import { CodeSnippet, CopyButton } from '@/ui/Code'23<CodeSnippet lang="bash" prompt="
quot;
>
4 npx @acme/cli deploy --project api-gateway5</CodeSnippet>67// The copy affordance is the component. It works anywhere a value has to8// be transcribed — this is why there is no separate "Copy to Clipboard".9<CopyButton value={deployment.id} label="Copy deployment ID" />1011// Copy from the source string. Never from the DOM: innerText drags in line12// numbers, the prompt, and the highlighter's markup.13function useCopy(timeout = 1600) {14 const [copied, setCopied] = React.useState(false)15 const copy = async (text: string) => {16 try {17 await navigator.clipboard.writeText(text)18 } catch {19 legacyCopy(text) // http:// and older Safari have no clipboard API20 }21 setCopied(true)22 setTimeout(() => setCopied(false), timeout)23 }24 return { copied, copy }25}

Framework-free HTML

html
<!-- Multi-line. The block is focusable because it scrolls. -->
<figure class="ds-code">
  <figcaption class="sr-only">Deployment example, TypeScript, 9 lines</figcaption>

  <pre tabindex="0" aria-label="Deployment example"><code class="language-ts"
    ><span class="ds-code__gutter" aria-hidden="true">1</span>const client = createClient({}</code></pre>

  <button type="button" class="ds-code__copy" aria-label="Copy code">
    <svg aria-hidden="true">…</svg>
  </button>
</figure>

<!-- The confirmation everyone forgets -->
<p class="sr-only" role="status" aria-live="polite">Copied</p>

<!-- Single line with a shell prompt. The $ is decoration. -->
<div class="ds-code ds-code--inline-block">
  <span class="ds-code__prompt" aria-hidden="true">$</span>
  <code>npm install @acme/sdk</code>
  <button type="button" aria-label="Copy command">…</button>
</div>

CSS

css
.ds-code {
  position: relative;
  border: 1px solid var(--ds-border-subtle);
  border-radius: var(--radius-lg);
  background: var(--ds-surface-inset);   /* a well, not a raised card */
}

.ds-code pre {
  margin: 0;
  padding: 14px;
  max-block-size: 24rem;                 /* ~24 lines, then scroll */
  overflow: auto;
  font-family: var(--font-mono);
  font-size: 13px;                       /* mono runs optically large */
  line-height: 1.55;                     /* looser than prose: scanned as columns */
  tab-size: 2;
}

/* Never selectable, never copied. This is the bug that ships most often. */
.ds-code__gutter,
.ds-code__prompt {
  user-select: none;
  -webkit-user-select: none;
  color: var(--ds-fg-disabled);          /* allowed: not content */
}

.ds-code__copy {
  position: absolute;
  inset-block-start: 8px;
  inset-inline-end: 8px;
  inline-size: 28px;
  block-size: 28px;
}

/* Hover-reveal is fine on a mouse and catastrophic on touch. */
@media (hover: hover) and (pointer: fine) {
  .ds-code__copy { opacity: 0; transition: opacity 120ms; }
  .ds-code:hover .ds-code__copy,
  .ds-code__copy:focus-visible { opacity: 1; }
}
@media (pointer: coarse) {
  .ds-code__copy { inline-size: 44px; block-size: 44px; }
}

/* No wrapping. A copied fragment with an invented newline is a broken command. */
.ds-code--inline-block code { white-space: nowrap; overflow-x: auto; }

Component API

CodeSnippet

PropTypeDefaultDescription
children*string—The exact source. This string is what gets copied — never the rendered DOM.
lang'tsx' | 'ts' | 'js' | 'html' | 'css' | 'bash' | 'json' | 'text''text'Drives highlighting and the announced language.
promptstring—A decorative shell prefix. aria-hidden and unselectable; never part of the copied value.
showLineNumbersbooleanfalseAdds an unselectable gutter. Only useful when the surrounding prose references line numbers.
wrapbooleanfalseOff for commands, where an invented newline breaks the paste. Acceptable for prose-like config.
maxHeightnumber460Pixel height past which the block scrolls rather than growing.

CopyButton

PropTypeDefaultDescription
value*string—Exactly what lands on the clipboard.
labelstring'Copy'Accessible name. Must identify the value on any page with more than one copy control.
timeoutnumber1600How long the confirmation holds, in ms.

Notes

Professional tips

  • Offer a package-manager switcher (npm / pnpm / yarn / bun) on install commands and remember the choice across the whole site. It is the single highest-value affordance on any docs page.
  • Show the copy control on focus as well as on hover, or keyboard users never discover it exists.
  • Truncate long single-line values in the middle rather than the end — the tail of a key is what people check against.
  • If a snippet contains a placeholder the user must replace, mark it visually and keep it in the copied string. Silently substituting their real key is worse than making them edit one word.

Performance

  • Highlight on the server or at build time where you can. A client-side highlighter on twenty blocks is a measurable chunk of main-thread time on first paint.
  • Do not re-highlight on every render. Memoise on the source string and the language, or a page of snippets re-tokenises on every keystroke elsewhere.
  • Virtualise blocks over roughly 500 lines — but a 500-line block in a doc page is a design problem before it is a performance one.
  • The clipboard API is async and can reject on an insecure origin. Always keep the legacy fallback; failing silently on http:// is a support ticket nobody can diagnose.

Common mistakes

  • Copying innerText, so line numbers and the shell prompt land on the clipboard.
  • No confirmation, so users press the button repeatedly and then paste to check.
  • A visual tick with no live-region announcement, leaving screen-reader users with no feedback at all.
  • Every copy button on the page named "Copy", so assistive tech reports six identical controls.
  • Hover-only copy controls, which are invisible on every touch device.
  • Syntax colours ported straight from a code editor, where comments routinely sit around 2.5:1.
  • A scrollable block with no tabindex, unreachable and unscrollable by keyboard.

Real-world recommendations

  • On docs pages, copy rate is one of the few honest engagement metrics — it means someone is actually running the thing rather than skimming.
  • API-key screens should show the value exactly once, mask it thereafter, and keep copy working while masked. Every product that forces a reveal to copy gets keys screen-shared into meetings.
  • For multi-step setup, give each step its own snippet with its own copy control. One block containing five commands guarantees someone runs all five when they only meant to run the third.
  • Include the expected output as a separate, non-copyable block. Users need to know what success looks like, and they must not be able to paste it back into a shell.