Copy button
Copy to the clipboard with confirmation and a live announcement; labels are passed in, so it carries no copy of its own.
ws_7f3a9c2e41b8pnpm exec shadcn add @intelligo/copy-buttonInside an app made by intelligo create, or any shadcn project that names the registry.
used by these pages
more in patterns
Source
What shadcn add writes to components/ui/copy-button.tsx. It is yours from then on.
"use client";
import * as React from "react";
import { CheckIcon, CopyIcon } from "lucide-react";
import { Button } from "@showcase/components/ui/button";
type CopyButtonProps = Omit<
React.ComponentProps<typeof Button>,
"onClick" | "value"
> & {
/** The text written to the clipboard. */
value: string;
/** Accessible name before copying — pass translated copy. */
label: string;
/** Announced once the text is on the clipboard. */
copiedLabel: string;
onCopy?: () => void;
onCopyError?: (error: unknown) => void;
/** How long the confirmation shows, in milliseconds. */
timeout?: number;
};
/**
* Copy with confirmation, the same everywhere. The
* icon swaps and a polite live region announces the copy; failures go to
* `onCopyError` so the caller decides whether to toast.
*/
function CopyButton({
value,
label,
copiedLabel,
onCopy,
onCopyError,
timeout = 2000,
variant = "ghost",
size = "icon-sm",
children,
...props
}: CopyButtonProps) {
const [copied, setCopied] = React.useState(false);
React.useEffect(() => {
if (!copied) return;
const timer = setTimeout(() => setCopied(false), timeout);
return () => clearTimeout(timer);
}, [copied, timeout]);
async function copy() {
try {
await navigator.clipboard.writeText(value);
setCopied(true);
onCopy?.();
} catch (error) {
onCopyError?.(error);
}
}
return (
<Button
data-slot="copy-button"
data-copied={copied || undefined}
variant={variant}
size={size}
aria-label={children ? undefined : copied ? copiedLabel : label}
onClick={copy}
{...props}
>
{copied ? (
<CheckIcon data-icon="inline-start" />
) : (
<CopyIcon data-icon="inline-start" />
)}
{children}
<span aria-live="polite" className="sr-only">
{copied ? copiedLabel : ""}
</span>
</Button>
);
}
export { CopyButton };Ready to try it? Three commands and the app runs against a stub model.