62 / Controls
Copy Button
Copies a value and says whether it worked, including when the clipboard refuses.
sk_live_7f3a91c2b8e04d5aa1Not copied yet
"use client"
import * as React from "react"
import { CopyButton } from "mischief-ui/copy-button"
const key = "sk_live_7f3a91c2b8e04d5aa1"
export function CopyButtonDemo() {
const [copied, setCopied] = React.useState(0)
return (
<div className="grid w-full max-w-md gap-4">
<div className="border-border bg-muted/40 flex items-center gap-2 rounded-[calc(var(--radius)+0.15rem)] border px-3 py-2">
<code className="min-w-0 flex-1 truncate font-[family-name:var(--font-mono),monospace] text-xs">
{key}
</code>
<CopyButton value={key} onCopied={() => setCopied((n) => n + 1)} />
</div>
<CopyButton
className="border-border w-fit border px-3"
value={key}
onCopied={() => setCopied((n) => n + 1)}
>
Copy key
</CopyButton>
<p className="text-muted-foreground text-xs" role="status">
{copied === 0 ? "Not copied yet" : `Copied ${copied}×`}
</p>
</div>
)
}Installation
Copy the source into your project, or keep it behind a package.
npx shadcn@latest add Tinkerers-Labs/mischief-ui/copy-buttonimport { CopyButton } from "mischief-ui/copy-button"Or paste it in yourself. The source imports the shared cn helper from @/lib/utils, so point that at your own copy.
"use client" import * as React from "react"import { Check, Clipboard } from "lucide-react" import { cn } from "@/lib/utils" export type CopyButtonProps = Omit< React.ButtonHTMLAttributes<HTMLButtonElement>, "children" | "value"> & { value: string /** Shown beside the icon. Omit for an icon-only control. */ children?: React.ReactNodeUsage
export function Key({ apiKey }) {
return (
<CopyButton value={apiKey} onCopied={() => track("key_copied")}>
Copy key
</CopyButton>
)
}When the clipboard refuses
navigator.clipboard.writeText rejects more often than it looks: a page served over http, a sandboxed frame, a browser that wants a user gesture it did not see, a permission that was denied. Left unhandled the button does nothing, says nothing, and leaves an unhandled rejection behind.
This one catches it, says so, and tells you through onCopyError. Reach for that when there is somewhere better to put the failure -- a toast, or a field the reader can select from by hand.
API
valuestringWhat lands on the clipboard.childrenReactNodeShown beside the icon. Omit for icon only.label, copiedLabel, errorLabelstringThe three things it can say.onCopied(value: string) => voidCalled after a copy that worked.onCopyError(error: unknown) => voidCalled when the clipboard refused.Accessibility
The outcome is announced through a polite live region whether or not the label is visible, so an icon-only button is not silent. With no visible text it takes the current wording as its accessible name, which changes to say what happened. Copying is one press, and nothing about it depends on hovering.