Copy button
ActionsCopy to clipboard with the failure path treated as a real path, and the result announced rather than implied by an icon.
Preview
npx create-next-app@latestControls
Notes
navigator.clipboard is undefined outside a secure context — plain http on a LAN address, an embedded webview, a locked-down browser. A button that says Copied there is lying about something the reader cannot check until they paste, so the catch says so instead.
The outcome goes through a visually hidden role=status. Swapping a clipboard glyph for a tick announces nothing at all.
The transient label is held in a ref and cleared on the next click, so an impatient double click cannot leave the button stuck saying Copied.
The timeout is cleared on unmount from an effect that only returns a cleanup. Nothing here sets state inside an effect, which the React Compiler rules treat as an error rather than a warning.
Source
ShowHide
"use client";
import { useEffect, useRef, useState } from "react";
import { cn } from "@/lib/cn";
/**
* Copy to clipboard, with the failure path treated as a real path.
*
* navigator.clipboard is undefined outside a secure context — plain http on a
* LAN address, an embedded webview, a locked-down browser. A button that shows
* "Copied" there is lying about something the reader cannot verify until they
* paste. So the catch says so instead.
*
* The transient label is held in a ref-based timer and cleared on the next
* click, so an impatient double click cannot leave the button stuck.
*/
const variants = {
solid: "bg-ink text-paper hover:opacity-85",
outline: "border border-control-border text-ink hover:border-ink hover:bg-paper-sunken",
ghost: "text-ink-muted hover:bg-paper-sunken hover:text-ink",
} as const;
export type CopyVariant = keyof typeof variants;
type Status = "idle" | "copied" | "failed";
function ClipboardIcon() {
return (
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<g stroke="currentColor" strokeWidth="1.3" strokeLinejoin="round">
<rect x="5.5" y="5.5" width="8" height="9" rx="1.6" />
<path d="M10.5 5.5v-2a1.6 1.6 0 0 0-1.6-1.6H4.1A1.6 1.6 0 0 0 2.5 3.5v5.4a1.6 1.6 0 0 0 1.6 1.6h1.4" />
</g>
</svg>
);
}
function CheckIcon() {
return (
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path
d="m3 8.5 3.2 3.2L13 5"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
export function CopyButton({
value,
label = "Copy",
copiedLabel = "Copied",
variant = "outline",
icon = true,
className,
}: {
value: string;
label?: string;
copiedLabel?: string;
variant?: CopyVariant;
icon?: boolean;
className?: string;
}) {
const [status, setStatus] = useState<Status>("idle");
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
// Cleanup only — no setState in an effect, which the React Compiler rules
// treat as an error rather than a warning.
useEffect(() => {
return () => {
if (timer.current) clearTimeout(timer.current);
};
}, []);
async function copy() {
if (timer.current) clearTimeout(timer.current);
try {
await navigator.clipboard.writeText(value);
setStatus("copied");
} catch {
setStatus("failed");
}
timer.current = setTimeout(() => setStatus("idle"), 2000);
}
return (
<span className={cn("inline-flex flex-col items-start gap-1.5", className)}>
<button
type="button"
onClick={copy}
className={cn(
"inline-flex items-center gap-2 rounded-full px-4 py-2 text-[13px] font-medium transition-all",
variants[variant],
)}
>
{icon && (status === "copied" ? <CheckIcon /> : <ClipboardIcon />)}
{status === "copied" ? copiedLabel : label}
</button>
{/* Announced, not inferred from the icon swap. Visually hidden because
the button label already says it on screen. */}
<span role="status" className="sr-only">
{status === "copied" ? `${copiedLabel} to clipboard` : ""}
</span>
{status === "failed" && (
<span className="text-[12px] text-accent">
Clipboard unavailable — select the text and copy manually.
</span>
)}
</span>
);
}
export default function Demo({
label = "Copy",
value = "npx create-next-app@latest",
variant = "outline",
icon = true,
}: {
label?: string;
value?: string;
variant?: CopyVariant;
icon?: boolean;
}) {
return (
<div className="flex w-full max-w-sm flex-col items-center gap-5">
<code className="w-full truncate rounded-lg border border-rule bg-paper-sunken px-4 py-3 font-mono text-[12.5px] text-ink-muted">
{value}
</code>
<CopyButton value={value} label={label} variant={variant} icon={icon} />
</div>
);
}
Next
Stat tileNext step
Tell me where the business is losing time.
No pitch deck and no discovery-call funnel. Describe the bottleneck and I will tell you plainly whether it is worth building something for, and roughly what that would take.