# Copyable Read-Only Field

> 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.

A shadcn/ui block from the [@tealmac](https://ui.tealmac.com/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/tealmac/copyable-readonly-field).

- Page, with a live preview: https://shadcn.exchange/r/tealmac/copyable-readonly-field
- Screenshot: https://shadcn.exchange/shot/tealmac/copyable-readonly-field/light.png
- Registry item: https://ui.tealmac.com/r/copyable-readonly-field.json
- Listed: 2026-11-01, source last changed 2026-11-01

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/tealmac/copyable-readonly-field.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`
- Registry items: `input`, `button`, `label`

## Files

### `registry/patterns/copyable-readonly-field/copyable-readonly-field.tsx`

```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>
  )
}
```
