A read-only input for values people need to copy (links, IDs, commands) with a Copy button that confirms in text and aria-live, selects everything on focus, and falls back when the clipboard is blocked. Use this when the value is for pasting elsewhere.
registry/patterns/copyable-readonly-field/copyable-readonly-field.tsx
"use client"
import * as React from "react"
import { CheckIcon, CopyIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
export interface CopyableReadOnlyFieldProps {
label?: string
value?: string
/** Text read out and shown after copying. */
copiedMessage?: string
onCopy?: (value: string) => void
}
export function CopyableReadOnlyField({
label = "Share link",
value = "https://example.com/share/8f2a91c4",
copiedMessage = "Link copied to the clipboard.",
onCopy,
}: CopyableReadOnlyFieldProps) {
const [state, setState] = React.useState<"idle" | "copied" | "blocked">(
"idle"
)
const inputRef = React.useRef<HTMLInputElement>(null)
async function copy() {
try {
await navigator.clipboard.writeText(value)
setState("copied")
onCopy?.(value)
setTimeout(() => setState("idle"), 2000)
} catch {
// Clipboard blocked (permissions, insecure origin): select the text so Ctrl+C works.
inputRef.current?.focus()
inputRef.current?.select()
setState("blocked")
}
}
return (
<div className="flex w-full max-w-md flex-col gap-1.5">
<Label htmlFor="crf-input">{label}</Label>
<div className="flex gap-2">
<Input
id="crf-input"
ref={inputRef}
readOnly
value={value}
className="font-mono text-sm"
onFocus={(e) => e.currentTarget.select()}
aria-describedby="crf-status"
/>
<Button
type="button"
variant="outline"
onClick={copy}
aria-label={
state === "copied" ? "Copied" : `Copy ${label.toLowerCase()}`
}
>
{state === "copied" ? (
<CheckIcon aria-hidden="true" />
) : (
<CopyIcon aria-hidden="true" />
)}
{state === "copied" ? "Copied" : "Copy"}
</Button>
</div>
<p
id="crf-status"
role="status"
className="text-muted-foreground text-xs"
>
{state === "copied"
? copiedMessage
: state === "blocked"
? "Copying was blocked. The text is selected: press Ctrl+C or Cmd+C."
: ""}
</p>
</div>
)
}