Copy Button
Original · freeA copy-to-clipboard button with a copied confirmation state.
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/btn-copy.json"use client";
import { useCallback, useEffect, useRef, useState } from "react";
type CopyState = "idle" | "copied" | "error";
function useCopy(text: string, resetMs = 2000) {
const [state, setState] = useState<CopyState>("idle");
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
return () => {
if (timer.current) clearTimeout(timer.current);
};
}, []);
const copy = useCallback(async () => {
if (timer.current) clearTimeout(timer.current);
try {
if (navigator?.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
} else {
const ta = document.createElement("textarea");
ta.value = text;
ta.setAttribute("readonly", "");
ta.style.position = "fixed";
ta.style.opacity = "0";
document.body.appendChild(ta);
ta.select();
document.execCommand("copy");
document.body.removeChild(ta);
}
setState("copied");
} catch {
setState("error");
}
timer.current = setTimeout(() => setState("idle"), resetMs);
}, [text, resetMs]);
return { state, copy };
}
function CopyGlyph() {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
className="h-4 w-4"
aria-hidden="true"
>
<rect x="9" y="9" width="11" height="11" rx="2" />
<path d="M6.5 15H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v1.5" />
</svg>
);
}
function CheckGlyph() {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2.4}
strokeLinecap="round"
strokeLinejoin="round"
className="h-4 w-4"
aria-hidden="true"
>
<path
d="M5 13l4 4L19 7"
className="btncopy-check-path"
pathLength={1}
/>
</svg>
);
}
function ErrorGlyph() {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2.2}
strokeLinecap="round"
strokeLinejoin="round"
className="h-4 w-4"
aria-hidden="true"
>
<circle cx="12" cy="12" r="9" />
<path d="M12 8v4M12 16h.01" />
</svg>
);
}
/* ---------- Variant 1: default labelled button ---------- */
function CopyButton({ text, label = "Copy" }: { text: string; label?: string }) {
const { state, copy } = useCopy(text);
const copied = state === "copied";
const errored = state === "error";
return (
<button
type="button"
onClick={copy}
aria-live="polite"
className={[
"group relative inline-flex items-center justify-center gap-2 overflow-hidden rounded-xl px-4 py-2.5 text-sm font-semibold",
"transition-all duration-200 ease-out active:translate-y-px",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
"focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950",
copied
? "bg-emerald-600 text-white ring-emerald-400 focus-visible:ring-emerald-500"
: errored
? "bg-rose-600 text-white ring-rose-400 focus-visible:ring-rose-500"
: "bg-slate-900 text-white shadow-sm shadow-slate-900/20 hover:bg-slate-800 focus-visible:ring-slate-500 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200 dark:focus-visible:ring-slate-300",
].join(" ")}
>
<span className="relative grid h-4 w-4 place-items-center">
<span
className={[
"col-start-1 row-start-1 transition-all duration-200",
copied || errored ? "scale-50 opacity-0" : "scale-100 opacity-100",
].join(" ")}
>
<CopyGlyph />
</span>
<span
className={[
"col-start-1 row-start-1 transition-all duration-200",
copied ? "scale-100 opacity-100" : "scale-50 opacity-0",
].join(" ")}
>
<CheckGlyph />
</span>
<span
className={[
"col-start-1 row-start-1 transition-all duration-200",
errored ? "scale-100 opacity-100" : "scale-50 opacity-0",
].join(" ")}
>
<ErrorGlyph />
</span>
</span>
<span className="tabular-nums">
{copied ? "Copied!" : errored ? "Failed" : label}
</span>
</button>
);
}
/* ---------- Variant 2: icon-only ghost button ---------- */
function CopyIconButton({ text }: { text: string }) {
const { state, copy } = useCopy(text);
const copied = state === "copied";
const errored = state === "error";
return (
<button
type="button"
onClick={copy}
aria-label={copied ? "Copied to clipboard" : "Copy to clipboard"}
title={copied ? "Copied" : "Copy"}
className={[
"inline-flex h-10 w-10 items-center justify-center rounded-xl border transition-all duration-200 active:translate-y-px",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
"focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950",
copied
? "border-emerald-500/40 bg-emerald-500/10 text-emerald-600 focus-visible:ring-emerald-500 dark:text-emerald-400"
: errored
? "border-rose-500/40 bg-rose-500/10 text-rose-600 focus-visible:ring-rose-500 dark:text-rose-400"
: "border-slate-200 bg-white text-slate-600 hover:bg-slate-50 hover:text-slate-900 focus-visible:ring-slate-400 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-300 dark:hover:bg-slate-800 dark:hover:text-white",
].join(" ")}
>
{copied ? <CheckGlyph /> : errored ? <ErrorGlyph /> : <CopyGlyph />}
</button>
);
}
/* ---------- Variant 3: code block with corner copy ---------- */
function CopyCodeBlock({ label, code }: { label: string; code: string }) {
const { state, copy } = useCopy(code);
const copied = state === "copied";
return (
<div className="overflow-hidden rounded-2xl border border-slate-200 bg-slate-50 shadow-sm dark:border-slate-800 dark:bg-slate-900/60">
<div className="flex items-center justify-between border-b border-slate-200 bg-white/70 px-4 py-2 dark:border-slate-800 dark:bg-slate-900/80">
<div className="flex items-center gap-2">
<span className="flex gap-1.5">
<span className="h-2.5 w-2.5 rounded-full bg-rose-400" />
<span className="h-2.5 w-2.5 rounded-full bg-amber-400" />
<span className="h-2.5 w-2.5 rounded-full bg-emerald-400" />
</span>
<span className="ml-1 font-mono text-xs text-slate-500 dark:text-slate-400">
{label}
</span>
</div>
<button
type="button"
onClick={copy}
aria-label={copied ? "Copied code" : "Copy code"}
className={[
"inline-flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-xs font-medium transition-all duration-200 active:translate-y-px",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
"focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950",
copied
? "text-emerald-600 focus-visible:ring-emerald-500 dark:text-emerald-400"
: "text-slate-500 hover:bg-slate-100 hover:text-slate-900 focus-visible:ring-slate-400 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-white",
].join(" ")}
>
{copied ? <CheckGlyph /> : <CopyGlyph />}
<span>{copied ? "Copied" : "Copy"}</span>
</button>
</div>
<pre className="overflow-x-auto px-4 py-3.5 font-mono text-[13px] leading-relaxed text-slate-800 dark:text-slate-200">
<code>{code}</code>
</pre>
</div>
);
}
/* ---------- Variant 4: inline token / API-key pill ---------- */
function CopyInlinePill({ value }: { value: string }) {
const { state, copy } = useCopy(value);
const copied = state === "copied";
return (
<div className="inline-flex items-center gap-1 rounded-full border border-slate-200 bg-white py-1 pl-3.5 pr-1 shadow-sm dark:border-slate-800 dark:bg-slate-900">
<code className="font-mono text-sm text-slate-700 dark:text-slate-200">
{value}
</code>
<button
type="button"
onClick={copy}
aria-label={copied ? "Copied value" : `Copy ${value}`}
className={[
"inline-flex h-7 w-7 items-center justify-center rounded-full transition-all duration-200 active:translate-y-px",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1",
"focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900",
copied
? "bg-emerald-500/15 text-emerald-600 focus-visible:ring-emerald-500 dark:text-emerald-400"
: "text-slate-400 hover:bg-slate-100 hover:text-slate-700 focus-visible:ring-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-100",
].join(" ")}
>
{copied ? <CheckGlyph /> : <CopyGlyph />}
</button>
</div>
);
}
/* ---------- Variant 5: accent gradient CTA ---------- */
function CopyAccentButton({ text }: { text: string }) {
const { state, copy } = useCopy(text);
const copied = state === "copied";
return (
<button
type="button"
onClick={copy}
aria-live="polite"
className={[
"relative inline-flex items-center justify-center gap-2 rounded-xl px-5 py-2.5 text-sm font-semibold text-white",
"transition-all duration-200 active:translate-y-px",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
"focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950",
copied
? "bg-emerald-600 focus-visible:ring-emerald-500"
: "bg-gradient-to-br from-indigo-500 to-violet-600 shadow-lg shadow-violet-500/25 hover:from-indigo-500 hover:to-violet-500 focus-visible:ring-violet-500",
].join(" ")}
>
{copied ? <CheckGlyph /> : <CopyGlyph />}
<span>{copied ? "Copied to clipboard" : "Copy invite link"}</span>
</button>
);
}
export default function BtnCopy() {
return (
<section className="relative w-full bg-white px-6 py-20 text-slate-900 dark:bg-slate-950 dark:text-slate-100 sm:px-8">
<style>{`
.btncopy-check-path {
stroke-dasharray: 1;
stroke-dashoffset: 1;
animation: btncopy-draw 260ms ease-out forwards;
}
@keyframes btncopy-draw {
to { stroke-dashoffset: 0; }
}
@media (prefers-reduced-motion: reduce) {
.btncopy-check-path { animation: none; stroke-dashoffset: 0; }
}
`}</style>
<div className="mx-auto max-w-3xl">
<header className="mb-12">
<span className="inline-flex items-center gap-1.5 rounded-full border border-slate-200 bg-slate-50 px-3 py-1 text-xs font-medium text-slate-500 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-400">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500" />
buttons / btn-copy
</span>
<h2 className="mt-4 text-2xl font-bold tracking-tight sm:text-3xl">
Copy to clipboard
</h2>
<p className="mt-2 max-w-prose text-sm leading-relaxed text-slate-500 dark:text-slate-400">
Tactile copy buttons with an animated confirmation state. Real
clipboard writes, focus-visible rings, and graceful failure
feedback.
</p>
</header>
<div className="space-y-10">
<div>
<p className="mb-3 text-xs font-medium uppercase tracking-wider text-slate-400 dark:text-slate-500">
Labelled & icon-only
</p>
<div className="flex flex-wrap items-center gap-3">
<CopyButton text="npm install motion" />
<CopyIconButton text="npm install motion" />
<CopyAccentButton text="https://freecode.dev/i/8fK2p" />
</div>
</div>
<div>
<p className="mb-3 text-xs font-medium uppercase tracking-wider text-slate-400 dark:text-slate-500">
Code block
</p>
<CopyCodeBlock
label="terminal"
code={`pnpm add motion\npnpm dlx shadcn@latest add button`}
/>
</div>
<div>
<p className="mb-3 text-xs font-medium uppercase tracking-wider text-slate-400 dark:text-slate-500">
Inline token
</p>
<div className="flex flex-wrap items-center gap-3">
<CopyInlinePill value="sk_live_51Nc8Qe7" />
<CopyInlinePill value="#6366F1" />
</div>
</div>
</div>
</div>
</section>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Shimmer Button
MITA dark pill button with a continuous conic-gradient light shimmer sweeping around its border.

Rainbow Gradient Button
MITA button wrapped in an animated multi-colour gradient border with a matching blurred underglow.

Interactive Hover Button
MITA pill button whose dot expands to fill the surface and reveals an arrow label on hover.

Pulsating Button
MITA solid button that emits a soft, continuously pulsing glow ring to draw the eye to the primary action.

Shiny Button
MITA frosted-glass button with a spring-animated light sweeping across its text and border.

Solid Set Button
OriginalA set of solid buttons: primary, secondary, success and danger, in three sizes.

Outline Set Button
OriginalOutline button variants across colours and sizes with a hover fill.

Ghost Set Button
OriginalGhost and text buttons with subtle hover backgrounds.

Soft Set Button
OriginalSoft, tinted buttons across semantic colours.

Gradient Set Button
OriginalGradient-filled buttons with a hover shift.
Icon Leading Button
OriginalButtons with leading and trailing icons.
Icon Only Button
OriginalSquare and circular icon-only buttons with tooltips and aria-labels.

