Copy Code Snippet
A dark code block for install commands: npm, pnpm, yarn and bun tabs, syntax-tinted tokens, line highlights, a copy button that really writes to the clipboard and only confirms when it did, and long lines that scroll under edge fades. No dependencies.
DOM + CSS0 deps1 file · 20 KB TSXreduced-motion aware
Props
"use client";
import { useEffect, useId, useRef, useState } from "react";
import type { KeyboardEvent, PointerEvent } from "react";
type PackageManager = "npm" | "pnpm" | "yarn" | "bun";
type Tok = { t: string; c: string; i?: boolean };
const CSN_MANAGERS: readonly PackageManager[] = ["npm", "pnpm", "yarn", "bun"];
const CSN_SHELL = new Set(["bash", "sh", "shell", "zsh", "terminal", "console"]);
const CSN_HASH = new Set(["py", "python", "yaml", "yml", "toml", "rb", "ruby", "env"]);
const CSN_SUBS = new Set(["add", "init", "install", "i", "run", "dlx", "exec", "create", "remove", "build", "dev", "start", "test", "update"]);
const CSN_KEYWORDS = new Set([
"const", "let", "var", "function", "return", "import", "from", "export", "default", "if", "else", "for", "while",
"async", "await", "new", "class", "extends", "type", "interface", "true", "false", "null", "undefined", "def", "in", "of",
]);
const CSN_COLOR = {
cmd: "#c4b5fd",
sub: "#f4f4f5",
flag: "#7dd3fc",
pkg: "#fcd34d",
str: "#86efac",
com: "#6b7280",
num: "#fdba74",
fn: "#7dd3fc",
key: "#93c5fd",
dim: "#71717a",
base: "#e4e4e7",
};
/** npm-flavoured lines rewritten for another package manager. */
function csnConvert(line: string, pm: PackageManager): string {
if (pm === "npm") return line;
const m = line.match(/^(\s*)(npx|npm)\s+(.*)$/);
if (!m) return line;
const ind = m[1] ?? "";
const bin = m[2] ?? "";
const rest = m[3] ?? "";
if (bin === "npx") return ind + (pm === "bun" ? "bunx" : pm + " dlx") + " " + rest;
const add = rest.match(/^(?:install|i|add)(?:\s+(.*))?$/);
if (add) return add[1] ? ind + pm + " add " + add[1] : ind + pm + " install";
const rm = rest.match(/^(?:uninstall|remove|rm)\s+(.*)$/);
if (rm) return ind + pm + " remove " + (rm[1] ?? "");
return ind + pm + " " + rest;
}
function csnShell(line: string): Tok[] {
const out: Tok[] = [];
const re = /(#.*$)|("[^"]*"|'[^']*')|(\s+)|(&&|\|\||[|;<>]+)|([^\s"'|;&<>]+)|(.)/g;
let expectCmd = true;
let afterCmd = false;
let m: RegExpExecArray | null;
while ((m = re.exec(line)) !== null) {
const [all, com, str, ws, op, word] = m;
if (com) out.push({ t: com, c: CSN_COLOR.com, i: true });
else if (str) out.push({ t: str, c: CSN_COLOR.str });
else if (ws) out.push({ t: ws, c: CSN_COLOR.base });
else if (op) {
out.push({ t: op, c: CSN_COLOR.dim });
expectCmd = !/[<>]/.test(op);
afterCmd = false;
} else if (word) {
if (expectCmd && /^[A-Za-z_]+=/.test(word)) {
out.push({ t: word, c: CSN_COLOR.dim });
} else if (expectCmd) {
out.push({ t: word, c: CSN_COLOR.cmd });
expectCmd = false;
afterCmd = true;
} else if (word.startsWith("-")) {
out.push({ t: word, c: CSN_COLOR.flag });
} else {
if (afterCmd && CSN_SUBS.has(word)) out.push({ t: word, c: CSN_COLOR.sub });
else if (/[@/]/.test(word)) {
const at = word.lastIndexOf("@");
if (at > 0) {
out.push({ t: word.slice(0, at), c: CSN_COLOR.pkg });
out.push({ t: word.slice(at), c: CSN_COLOR.dim });
} else out.push({ t: word, c: CSN_COLOR.pkg });
} else if (/^\d/.test(word)) out.push({ t: word, c: CSN_COLOR.num });
else out.push({ t: word, c: CSN_COLOR.base });
afterCmd = false;
}
} else out.push({ t: all, c: CSN_COLOR.base });
}
return out;
}
function csnGeneric(line: string, hash: boolean): Tok[] {
const out: Tok[] = [];
const re = /(\/\/.*$)|(#.*$)|("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)|(\b\d+(?:\.\d+)?\b)|([A-Za-z_$][\w$]*)|(\s+)|(.)/g;
let m: RegExpExecArray | null;
while ((m = re.exec(line)) !== null) {
const [all, slash, pound, str, num, id] = m;
const rest = line.slice(m.index + all.length);
if (slash) out.push({ t: slash, c: CSN_COLOR.com, i: true });
else if (pound) out.push(hash ? { t: pound, c: CSN_COLOR.com, i: true } : { t: pound, c: CSN_COLOR.base });
else if (str) out.push({ t: str, c: /^\s*:/.test(rest) ? CSN_COLOR.key : CSN_COLOR.str });
else if (num) out.push({ t: num, c: CSN_COLOR.num });
else if (id) {
if (CSN_KEYWORDS.has(id)) out.push({ t: id, c: CSN_COLOR.cmd });
else if (rest.startsWith("(")) out.push({ t: id, c: CSN_COLOR.fn });
else out.push({ t: id, c: CSN_COLOR.base });
} else out.push({ t: all, c: /^[^\w\s]$/.test(all) ? CSN_COLOR.dim : CSN_COLOR.base });
}
return out;
}
async function csnWrite(text: string): Promise<boolean> {
try {
if (typeof navigator !== "undefined" && navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
await navigator.clipboard.writeText(text);
return true;
}
} catch {
// Permission denied or insecure context: fall through to the legacy path.
}
if (typeof document === "undefined") return false;
const prev = document.activeElement as HTMLElement | null;
const ta = document.createElement("textarea");
ta.value = text;
ta.setAttribute("readonly", "");
ta.setAttribute("aria-hidden", "true");
ta.style.cssText = "position:fixed;top:-1000px;left:0;opacity:0;pointer-events:none;";
document.body.appendChild(ta);
let ok = false;
try {
ta.select();
ta.setSelectionRange(0, text.length);
ok = document.execCommand("copy");
} catch {
ok = false;
}
document.body.removeChild(ta);
if (prev && typeof prev.focus === "function") prev.focus({ preventScroll: true });
return ok;
}
const CSN_CSS = [
".csn{container-type:inline-size;position:relative;color:#e4e4e7;font-family:inherit;-webkit-tap-highlight-color:transparent}",
".csn ::selection{background:rgba(125,211,252,.28)}",
".csn-glow,.csn-ring{position:absolute;inset:0;pointer-events:none;opacity:0;transition:opacity .35s ease;border-radius:inherit}",
".csn-glow{background:radial-gradient(260px circle at var(--mx,50%) var(--my,0%),rgba(125,211,252,.09),transparent 70%)}",
".csn-ring{padding:1px;background:radial-gradient(180px circle at var(--mx,50%) var(--my,0%),rgba(186,230,253,.55),transparent 70%);-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask:linear-gradient(#000 0 0) content-box exclude,linear-gradient(#000 0 0)}",
".csn:hover .csn-glow,.csn:hover .csn-ring,.csn:focus-within .csn-glow,.csn:focus-within .csn-ring{opacity:1}",
".csn-head{position:relative;display:flex;align-items:center;justify-content:space-between;gap:8px;padding:8px 8px 8px 8px;border-bottom:1px solid rgba(255,255,255,.07)}",
".csn-tabs{position:relative;display:grid;grid-auto-flow:column;grid-auto-columns:1fr;padding:3px;border-radius:10px;background:rgba(255,255,255,.04);box-shadow:inset 0 0 0 1px rgba(255,255,255,.06)}",
".csn-pill{position:absolute;top:3px;bottom:3px;left:3px;border-radius:7px;background:rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.12),0 1px 2px rgba(0,0,0,.4);transition:transform .34s cubic-bezier(.34,1.32,.64,1)}",
".csn-t{position:relative;z-index:1;min-height:32px;padding:0 12px;border:0;border-radius:7px;background:transparent;color:#8b8d96;font:inherit;font-size:12.5px;font-weight:500;letter-spacing:.01em;cursor:pointer;transition:color .2s ease}",
".csn-t:hover{color:#d4d4d8}",
".csn-t[aria-selected=true]{color:#fff}",
".csn-lang{padding:3px 8px;border-radius:999px;background:rgba(255,255,255,.06);color:#9a9ca6;font-size:10.5px;font-weight:600;letter-spacing:.08em;text-transform:uppercase}",
".csn-right{display:flex;align-items:center;gap:10px}",
".csn-copy{position:relative;display:inline-flex;align-items:center;gap:7px;min-height:32px;padding:0 12px 0 10px;border:1px solid rgba(255,255,255,.1);border-radius:9px;background:rgba(255,255,255,.05);color:#d4d4d8;font:inherit;font-size:12.5px;font-weight:500;cursor:pointer;transition:background .2s ease,border-color .25s ease,color .25s ease,transform .15s ease}",
".csn-copy:hover{background:rgba(255,255,255,.1);color:#fff}",
".csn-copy:active{transform:scale(.95)}",
".csn-copy[data-status=copied]{background:rgba(16,185,129,.14);border-color:rgba(52,211,153,.4);color:#6ee7b7}",
".csn-copy[data-status=failed]{background:rgba(244,63,94,.14);border-color:rgba(251,113,133,.4);color:#fda4af}",
".csn button:focus-visible{outline:2px solid #7dd3fc;outline-offset:2px}",
".csn-t:focus-visible{border-radius:7px}",
".csn-copy:focus-visible{border-radius:9px}",
".csn-stack{display:grid}",
".csn-stack>*{grid-area:1/1;transition:opacity .22s ease,transform .3s cubic-bezier(.2,.8,.2,1),filter .22s ease}",
".csn-icons{width:16px;height:16px;place-items:center}",
".csn-stack>[data-on=false]{opacity:0;transform:translateY(5px) scale(.7);filter:blur(2px)}",
".csn-labels>[data-on=false]{transform:translateY(7px)}",
".csn-stack>[data-on=true]{opacity:1;transform:none;filter:none}",
".csn-check{stroke-dasharray:22;stroke-dashoffset:22;transition:stroke-dashoffset .32s cubic-bezier(.65,0,.35,1) .08s}",
".csn-stack>[data-on=true] .csn-check{stroke-dashoffset:0}",
".csn-scroll{overflow-x:auto;overflow-y:hidden;scrollbar-width:none;-ms-overflow-style:none;-webkit-mask-image:linear-gradient(90deg,transparent 0,#000 var(--fl,0px),#000 calc(100% - var(--fr,0px)),transparent 100%);mask-image:linear-gradient(90deg,transparent 0,#000 var(--fl,0px),#000 calc(100% - var(--fr,0px)),transparent 100%)}",
".csn-scroll::-webkit-scrollbar{display:none}",
".csn-scroll:focus-visible{outline:2px solid #7dd3fc;outline-offset:-2px;border-radius:0}",
".csn-code{margin:0;padding:14px 0;width:max-content;min-width:100%;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,'Liberation Mono',monospace;font-size:13px;line-height:1.75;tab-size:2;animation:csn-in .28s cubic-bezier(.2,.8,.2,1) both}",
".csn-line{display:flex;align-items:baseline;padding:0 18px;white-space:pre;border-left:2px solid transparent;margin-left:0}",
".csn-line[data-hl=true]{background:linear-gradient(90deg,rgba(125,211,252,.12),rgba(125,211,252,.03));border-left-color:#7dd3fc}",
".csn-gut{flex:none;user-select:none;-webkit-user-select:none;color:#52525b;text-align:right;min-width:2ch;margin-right:16px}",
".csn-sweep{position:absolute;top:0;left:0;width:45%;height:1px;pointer-events:none;background:linear-gradient(90deg,transparent,#6ee7b7,transparent);box-shadow:0 0 14px 1px rgba(110,231,183,.55);animation:csn-sweep .85s cubic-bezier(.4,0,.2,1) both}",
".csn-wash{position:absolute;left:0;right:0;top:0;height:70%;pointer-events:none;background:linear-gradient(180deg,rgba(16,185,129,.12),transparent);animation:csn-wash 1s ease-out both}",
"@keyframes csn-in{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:none}}",
"@keyframes csn-sweep{from{transform:translateX(-100%);opacity:1}80%{opacity:1}to{transform:translateX(230%);opacity:0}}",
"@keyframes csn-wash{from{opacity:1}to{opacity:0}}",
"@media (pointer:coarse){.csn-t,.csn-copy{min-height:40px}.csn-t{padding:0 13px}}",
"@container (max-width:380px){.csn-lang{display:none}.csn-t{padding:0 10px}}",
"@media (prefers-reduced-motion:reduce){.csn *,.csn *::before{animation:none!important;transition-duration:.01ms!important;transition-delay:0s!important}.csn-sweep,.csn-wash{display:none}.csn-stack>[data-on=false]{transform:none;filter:none}}",
].join("\n");
export function CopyCodeSnippet({
code = "npx shadcn@latest add viberdy-dev/ui/copy-code-snippet",
language = "bash",
packageTabs = true,
defaultTab = "npm",
lineNumbers,
highlight,
className = "",
onCopy,
}: {
/** The snippet. Line breaks are kept exactly as given. */
code?: string;
/** Label pill and tokenizer hint (bash, ts, json, python ...). */
language?: string;
/** Show npm / pnpm / yarn / bun tabs when the snippet contains npx or npm lines. */
packageTabs?: boolean;
defaultTab?: PackageManager;
/** Gutter numbers. Left undefined: on for multi-line code that is not a shell command. */
lineNumbers?: boolean;
/** 1-based line numbers to mark. */
highlight?: number[];
className?: string;
onCopy?: (text: string) => void;
}) {
const uid = useId();
const scroller = useRef<HTMLDivElement>(null);
const codeEl = useRef<HTMLPreElement>(null);
const tabRefs = useRef<(HTMLButtonElement | null)[]>([]);
const timer = useRef(0);
const [pm, setPm] = useState<PackageManager>(defaultTab);
const [status, setStatus] = useState<"idle" | "copied" | "failed">("idle");
const [pulse, setPulse] = useState(0);
const lang = language.toLowerCase();
const shell = CSN_SHELL.has(lang);
const raw = code.replace(/\r\n/g, "\n").replace(/\n+$/, "");
const hasTabs = packageTabs && shell && /^\s*(npx|npm)\s/m.test(raw);
const active: PackageManager = hasTabs ? pm : "npm";
const lines = raw.split("\n").map((l) => (hasTabs ? csnConvert(l, active) : l));
const text = lines.join("\n");
const marked = new Set(highlight ?? []);
const numbered = lineNumbers ?? (!shell && lines.length > 1);
const tabIndex = Math.max(0, CSN_MANAGERS.indexOf(active));
const panelId = "csn-panel-" + uid.replace(/[^a-zA-Z0-9]/g, "");
useEffect(() => {
return () => window.clearTimeout(timer.current);
}, []);
// Edge fades follow how much code is hidden on each side.
useEffect(() => {
const el = scroller.current;
if (!el) return;
const update = () => {
const max = el.scrollWidth - el.clientWidth;
el.style.setProperty("--fl", Math.min(36, Math.max(0, el.scrollLeft)) + "px");
el.style.setProperty("--fr", Math.min(36, Math.max(0, max - el.scrollLeft)) + "px");
el.tabIndex = max > 1 ? 0 : -1;
};
update();
el.addEventListener("scroll", update, { passive: true });
let ro: ResizeObserver | null = null;
if (typeof ResizeObserver !== "undefined") {
ro = new ResizeObserver(update);
ro.observe(el);
if (el.firstElementChild) ro.observe(el.firstElementChild);
}
return () => {
el.removeEventListener("scroll", update);
ro?.disconnect();
};
}, [text, active]);
async function copy() {
const ok = await csnWrite(text);
window.clearTimeout(timer.current);
if (ok) {
setStatus("copied");
setPulse((n) => n + 1);
onCopy?.(text);
} else {
setStatus("failed");
const sel = window.getSelection();
if (sel && codeEl.current) {
const range = document.createRange();
range.selectNodeContents(codeEl.current);
sel.removeAllRanges();
sel.addRange(range);
}
}
timer.current = window.setTimeout(() => setStatus("idle"), ok ? 1900 : 2600);
}
function onMove(e: PointerEvent<HTMLDivElement>) {
const el = e.currentTarget;
const r = el.getBoundingClientRect();
const k = r.width > 0 ? el.offsetWidth / r.width : 1;
el.style.setProperty("--mx", (e.clientX - r.left) * k + "px");
el.style.setProperty("--my", (e.clientY - r.top) * k + "px");
}
function onTabKey(e: KeyboardEvent<HTMLDivElement>) {
let next = tabIndex;
if (e.key === "ArrowRight") next = (tabIndex + 1) % CSN_MANAGERS.length;
else if (e.key === "ArrowLeft") next = (tabIndex - 1 + CSN_MANAGERS.length) % CSN_MANAGERS.length;
else if (e.key === "Home") next = 0;
else if (e.key === "End") next = CSN_MANAGERS.length - 1;
else return;
e.preventDefault();
const target = CSN_MANAGERS[next];
if (target) setPm(target);
tabRefs.current[next]?.focus();
}
const label = hasTabs ? "Copy " + active + " command" : "Copy code";
return (
<>
<style>{CSN_CSS}</style>
<div
onPointerMove={onMove}
className={"csn w-full max-w-md overflow-hidden rounded-2xl " + className}
style={{
background: "linear-gradient(180deg,#101116,#0a0b0e)",
boxShadow:
"0 0 0 1px rgba(255,255,255,.09), inset 0 1px 0 rgba(255,255,255,.07), 0 1px 2px rgba(0,0,0,.25), 0 28px 60px -28px rgba(0,0,0,.65)",
}}
>
<div aria-hidden className="csn-glow" />
<div aria-hidden className="csn-ring" />
{pulse > 0 && status === "copied" ? (
<div key={pulse} aria-hidden>
<div className="csn-wash" />
<div className="csn-sweep" />
</div>
) : null}
<div className="csn-head">
{hasTabs ? (
<div role="tablist" aria-label="Package manager" className="csn-tabs" onKeyDown={onTabKey}>
<span
aria-hidden
className="csn-pill"
style={{
width: "calc((100% - 6px) / " + CSN_MANAGERS.length + ")",
transform: "translateX(" + tabIndex * 100 + "%)",
}}
/>
{CSN_MANAGERS.map((m, i) => (
<button
key={m}
ref={(el) => {
tabRefs.current[i] = el;
}}
type="button"
role="tab"
id={panelId + "-t" + m}
aria-selected={m === active}
aria-controls={panelId}
tabIndex={m === active ? 0 : -1}
className="csn-t"
onClick={() => setPm(m)}
>
{m}
</button>
))}
</div>
) : (
<span className="csn-lang">{language}</span>
)}
<div className="csn-right">
{hasTabs ? <span className="csn-lang">{language}</span> : null}
<button type="button" className="csn-copy" data-status={status} aria-label={label} onClick={copy}>
<span aria-hidden className="csn-stack csn-icons">
<svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" data-on={status === "idle"}>
<rect x="5.5" y="5.5" width="8" height="8" rx="2" />
<path d="M10.5 3.5v-.2A1.8 1.8 0 0 0 8.7 1.5H3.8A1.8 1.8 0 0 0 2 3.3v4.9a1.8 1.8 0 0 0 1.8 1.8h.2" />
</svg>
<svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round" data-on={status === "copied"}>
<path className="csn-check" pathLength={22} d="M3 8.4l3.2 3.2L13 4.6" />
</svg>
<svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" data-on={status === "failed"}>
<path d="M4 4l8 8M12 4l-8 8" />
</svg>
</span>
<span aria-hidden className="csn-stack csn-labels">
<span data-on={status === "idle"}>Copy</span>
<span data-on={status === "copied"}>Copied</span>
<span data-on={status === "failed"}>Failed</span>
</span>
</button>
</div>
</div>
<div id={panelId} role={hasTabs ? "tabpanel" : undefined} aria-labelledby={hasTabs ? panelId + "-t" + active : undefined}>
<div ref={scroller} className="csn-scroll" role="region" aria-label={language + " code"}>
<pre ref={codeEl} key={active} className="csn-code">
{lines.map((line, i) => (
<span key={i} className="csn-line" data-hl={marked.has(i + 1)}>
{numbered ? (
<span aria-hidden className="csn-gut">
{i + 1}
</span>
) : shell && line.trim() !== "" && !line.trim().startsWith("#") ? (
<span aria-hidden className="csn-gut" style={{ minWidth: "1ch", marginRight: 10 }}>
$
</span>
) : null}
<code style={{ font: "inherit" }}>
{(shell ? csnShell(line) : csnGeneric(line, CSN_HASH.has(lang))).map((tok, j) => (
<span key={j} style={{ color: tok.c, fontStyle: tok.i ? "italic" : undefined }}>
{tok.t}
</span>
))}
{line === "" ? " " : null}
</code>
</span>
))}
</pre>
</div>
</div>
<span className="sr-only" role="status" aria-live="polite">
{status === "copied" ? "Copied to clipboard" : status === "failed" ? "Copy failed. The code is selected, press Ctrl+C." : ""}
</span>
</div>
</>
);
}
Free. Pro unlocks everything, from $27/month.
Details
About this pattern
A copy button earns trust by being honest. Clicking it calls navigator.clipboard.writeText; if that is missing or rejected (an insecure page, a denied permission, an embedded frame) it falls back to a hidden textarea and execCommand, and only when one of the two succeeds does the button morph to a drawn check with an emerald sweep along the top edge. If both fail it says Failed and selects the code so Ctrl+C works. Paste an npx or npm command and the block grows npm, pnpm, yarn and bun tabs: the same lines are rewritten (npx becomes pnpm dlx, yarn dlx or bunx; npm install becomes add) and the copy button always copies the command in the tab you are looking at. The tabs slide a pill with a slight overshoot and the code swaps with a short rise. Tokens are tinted by a small built-in tokenizer: commands, flags, packages, strings, comments and numbers for shell, plus keywords, calls and JSON keys for other languages, with no highlighter library. Lines you list in highlight get a left rule and a wash. Long lines scroll sideways under edge fades that appear only on the side with more code, instead of clipping. A faint light follows the pointer along the border.
Curator’s note
Copy Code Snippet, rebuilt in October. The first version flipped to Copied even when the browser refused the copy; this one only confirms when the text really reached the clipboard, and needs nothing but React.
