# Copyable Field

A selectable, horizontally scrollable field with a copy action.

## Installation

```bash
npx shadcn@latest add https://ui.jarv.is/r/copyable-field.json
```

[Registry JSON](https://ui.jarv.is/r/copyable-field.json)

## Preview

```tsx
import { CopyableField } from "@/components/ui/copyable-field";

export function Preview() {
  return (
    <div className="flex w-full max-w-xl flex-col gap-4">
      <CopyableField
        label="Install"
        value="bunx --bun shadcn@latest add https://ui.jarv.is/r/copyable-field.json"
      />
      <CopyableField label="Token" value="jui_live_7vK8x8e8f95hK2hVv4sYQxB3" showLabel={false} />
    </div>
  );
}
```


## Source

### ui/copyable-field.tsx

```tsx
"use client";

import * as React from "react";

import { CopyButton } from "@/components/ui/copy-button";
import { Field, FieldLabel } from "@/components/ui/field";
import { InputGroup, InputGroupAddon } from "@/components/ui/input-group";
import { ScrollArea } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils";

type CopyableFieldCopyEvent = Parameters<
  NonNullable<React.ComponentProps<typeof CopyButton>["onCopied"]>
>[1];

type CopyableFieldProps = Omit<React.ComponentProps<typeof Field>, "children" | "onCopy"> & {
  label: React.ReactNode;
  value: string;
  copyValue?: string;
  showLabel?: boolean;
  copyLabel?: string;
  copiedLabel?: string;
  resetDelay?: number;
  children?: React.ReactNode;
  onCopy?: (value: string, event: CopyableFieldCopyEvent) => void;
  onCopyError?: (error: unknown, event: CopyableFieldCopyEvent) => void;
};

function CopyableField({
  label,
  value,
  copyValue = value,
  showLabel = true,
  copyLabel = "Copy to clipboard",
  copiedLabel = "Copied",
  resetDelay = 2000,
  children,
  className,
  onCopy,
  onCopyError,
  ...props
}: CopyableFieldProps) {
  const contentRef = React.useRef<HTMLSpanElement>(null);

  const handleSelect = React.useCallback(() => {
    if (!contentRef.current) {
      return;
    }

    const selection = window.getSelection();

    if (!selection) {
      return;
    }

    const range = document.createRange();

    range.selectNodeContents(contentRef.current);
    selection.removeAllRanges();
    selection.addRange(range);
  }, []);

  return (
    <Field className={cn("min-w-0", className)} {...props}>
      <FieldLabel className={cn(showLabel ? "text-xs text-muted-foreground uppercase" : "sr-only")}>
        {label}
      </FieldLabel>
      <InputGroup className="h-10 min-w-0">
        <ScrollArea className="h-full min-w-0 flex-1 [&_[data-slot=scroll-area-scrollbar]]:hidden">
          <button
            type="button"
            aria-label={typeof label === "string" ? `Select ${label}` : "Select text"}
            onClick={handleSelect}
            className="h-full w-full min-w-0 cursor-text bg-transparent py-0 pr-2 pl-3 text-left font-mono text-[13px] outline-none"
          >
            <span ref={contentRef} className="inline-block whitespace-nowrap">
              {children ?? value}
            </span>
          </button>
        </ScrollArea>
        <InputGroupAddon align="inline-end">
          <CopyButton
            value={copyValue}
            size="icon-xs"
            copyLabel={copyLabel}
            copiedLabel={copiedLabel}
            resetDelay={resetDelay}
            onCopied={onCopy}
            onCopyError={onCopyError}
          />
        </InputGroupAddon>
      </InputGroup>
    </Field>
  );
}

export { CopyableField, type CopyableFieldProps };
```



## Usage

### Commands and tokens

Use a copyable field for install commands, API keys, and long single-line values.

```tsx
import { CopyableField } from "@/components/ui/copyable-field";

export function Example() {
  return <CopyableField label="Install" value="bunx --bun shadcn@latest add ..." />;
}
```

### Separate display and copy values

Use `copyValue` when the visible text should be shortened but the copied value should stay exact.

```tsx
import { CopyableField } from "@/components/ui/copyable-field";

export function ApiKeyField() {
  return (
    <CopyableField
      label="API key"
      value="jui_live_7vK8x...xB3"
      copyValue="jui_live_7vK8x8e8f95hK2hVv4sYQxB3"
      copyLabel="Copy API key"
    />
  );
}
```

### Hidden label

Hide the visual label when the surrounding UI already provides context. The label remains
available to assistive technology.

```tsx
import { CopyableField } from "@/components/ui/copyable-field";

export function CompactTokenField({ token }: { token: string }) {
  return <CopyableField label="Project token" value={token} showLabel={false} />;
}
```

### Custom rendered value

Render children when you need syntax highlighting or visual masking, while keeping the copied value
plain.

```tsx
import { CopyableField } from "@/components/ui/copyable-field";

export function HighlightedCommand() {
  return (
    <CopyableField label="Install" value="pnpm dlx shadcn@latest add https://ui.jarv.is/r/toast.json">
      <span className="text-muted-foreground">pnpm dlx</span>{" "}
      shadcn@latest add https://ui.jarv.is/r/toast.json
    </CopyableField>
  );
}
```

### Copy callbacks

Use callbacks to track successful copies or surface clipboard failures.

```tsx
import { CopyableField } from "@/components/ui/copyable-field";

export function TrackedCopyField({ command }: { command: string }) {
  return (
    <CopyableField
      label="CLI command"
      value={command}
      onCopy={(value) => {
        console.log(`Copied ${value}`);
      }}
      onCopyError={(error) => {
        console.error("Could not copy command", error);
      }}
    />
  );
}
```

