Home / Snippets / Typography /

Copy button on code block

Use position: relative on the wrapper and position: absolute on the button to overlay a copy control in the top-right corner of any code block.

const greet = (name) => {
  return `Hello, ${name}!`;
};

console.log(greet("world"));
Widely Supported
typographyuino-js

Quick implementation

.code-block {
  position: relative;
  background: oklch(0.17 0.02 260);
  border-radius: 0.5rem;
  border: 1px solid oklch(0.28 0.03 265);
  padding: 1.25rem;
}

.code-block pre {
  margin: 0;
  font-family: monospace;
  font-size: 0.875rem;
  line-height: 1.65;
  overflow-x: auto;
}

.code-block .copy-btn {
  position: absolute;
  top: 0.5rem;
  right: 0.5rem;
  padding: 0.3rem 0.75rem;
  min-width: 2.75rem;
  min-height: 2rem;
  font-size: 0.78rem;
  font-weight: 600;
  background: oklch(0.28 0.06 265);
  color: oklch(0.88 0.05 265);
  border: 1px solid oklch(0.38 0.08 265);
  border-radius: 0.35rem;
  cursor: pointer;
  z-index: 1;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}

.code-block .copy-btn:hover {
  background: oklch(0.36 0.1 265);
  color: oklch(0.96 0.02 265);
  border-color: oklch(0.52 0.14 265);
}

.code-block .copy-btn:active {
  background: oklch(0.42 0.14 265);
}

Prompt this to your LLM

Includes role, constraints, two framework variants, and edge cases to handle.

You are a senior frontend engineer building a documentation site.

Goal: A code block component with a positioned "Copy" button in the
top-right corner — pure CSS layout, with optional clipboard JavaScript.

Technical constraints:
- Set position: relative on the .code-block wrapper so the button
  establishes its own containing block.
- Set position: absolute; top: 0.5rem; right: 0.5rem on .copy-btn.
- Give the button a min-height of 2rem and min-width of 2.75rem for
  a comfortable touch target (WCAG 2.5.8 target size).
- Use oklch() for all colors — no hex or rgba values.
- Style hover and active states with smooth transitions (0.15s ease).
- Add z-index: 1 so the button stays above any syntax-highlighted tokens.
- The <pre> element should have overflow-x: auto for long lines and
  tabindex="0" for keyboard scrollability.

Framework variant (pick one):
A) Vanilla HTML/CSS — .code-block wrapper with a <button class="copy-btn">
   and a <pre><code> block.
B) Web Component — <copy-code-block> that wraps slotted <pre> content,
   injects the button into shadow DOM, and handles clipboard write.

Edge cases to handle:
- Very long single lines inside <pre> should scroll horizontally without
  affecting the button's position — the button is in normal flow above
  the pre, not inside it.
- On small viewports, confirm the button does not overlap the first line
  of code — add enough padding-top to the wrapper (e.g. 2.5rem) when
  the button is absolutely positioned over content.
- If the copy action succeeds, temporarily change the button label to
  "Copied!" and restore it after 1.5s using setTimeout.

Return CSS only (or a Web Component if variant B is chosen).

Why positioned overlay beats flex or grid for this pattern

The first instinct for placing a button alongside a code block is often a flex row: put the button and the <pre> as siblings and push the button to the right with margin-left: auto. That works for a toolbar above the block, but it changes the document flow — the button takes up vertical space, the code sits below it, and the two never visually overlap. The positioned overlay approach layers the button on top of the code block without disturbing the block's natural height or line count.

Grid faces the same constraint. You could place the button in a named grid area alongside the <pre>, but that forces the grid row to be at least as tall as the button, adding unwanted padding above the first line of code unless you resort to negative margins.

With position: relative on the wrapper and position: absolute on the button, the button is completely removed from flow. The wrapper's height is determined solely by the <pre> content, and the button floats over the top-right corner. Adding padding-top: 2.5rem to the wrapper on small screens (or when content is short) ensures the button never obscures the first line of code.

Z-index considerations

Most of the time you don't need a z-index on the copy button — absolute positioning already lifts it above its in-flow siblings. But syntax highlighters often wrap tokens in <span> elements with their own stacking contexts, and some libraries set explicit z-index values on those spans. A z-index: 1 on the button is a low-cost insurance policy: it guarantees the button paints above any token spans without creating a stacking context heavyweight that could interfere with modals or tooltips elsewhere on the page.

If your code block wrapper itself has a transform, filter, or will-change property (common for entrance animations), it becomes a new stacking context. Inside that context, the button's z-index only needs to be higher than other children — it won't compete with page-level overlays. This is usually what you want: the button stays on top of the code, and the entire block can slide or fade as a unit without the button escaping the animation.

Touch target sizing

A copy button is interactive UI, not decoration. WCAG 2.5.8 (Target Size, Level AA in WCAG 2.2) recommends a minimum target size of 24×24 CSS pixels, with the practical guidance from many design systems sitting at 44×44 pixels for primary actions. A code-block copy button is secondary — users don't tap it on every page load — so 44×44 is not strictly required, but a min-height: 2rem (32 px at default font size) and min-width: 2.75rem are reasonable minimums that keep the button easily tappable on a phone.

Use padding rather than a fixed height so the button grows gracefully if the label changes (e.g. "Copied!" is slightly wider than "Copy"). Pair this with display: inline-flex; align-items: center; justify-content: center to keep the label vertically centred regardless of padding. The result is a button that always meets minimum touch target requirements and never clips its label.