Patterns
Copy
A button, a field and a secret field that copy a value to the clipboard.
Keep this key on your server.
import { CopyField, Field, SecretField } from 'ferry-ui'
export default function CopyHero() {
return (
<div className="mx-auto flex w-full max-w-md flex-col gap-5">
<Field label="Project ID">
<CopyField value="prj_7Hq2kLx9Vd3mN4" what="project ID" />
</Field>
<Field label="Secret key" hint="Keep this key on your server.">
<SecretField value="sk_demo_4f9a2c7e1b8d3f6a" what="secret key" />
</Field>
</div>
)
}SecretField.Import the component that you need. Each one has one part.
import { CopyButton, CopyField, SecretField } from 'ferry-ui'
<CopyButton value="" what="" />
<CopyField value="" what="" />
<SecretField value="" what="" />| Component | Role |
|---|---|
CopyButton | A small button next to a value. |
CopyField | A read-only field with a Copy button. |
SecretField | A read-only field that masks its value. It has a reveal button and a Copy button. |
By default, CopyButton shows only an icon and a tooltip. Set what to name the value. After a copy, the icon is a check mark for 1.5 seconds. Use variant="ghost" in a dense row or a table.
import { CopyButton } from 'ferry-ui'
const INVOICES = ['INV-2026-0142', 'INV-2026-0143']
export default function CopyButtonIcon() {
return (
<ul className="w-full max-w-xs divide-y rounded-lg border bg-surface-100">
{INVOICES.map((invoice) => (
<li key={invoice} className="flex items-center justify-between gap-3 px-4 py-2">
<span className="font-mono text-[13px] text-foreground">{invoice}</span>
{/* `what` gives the tooltip and the accessible name: "Copy invoice number". */}
<CopyButton value={invoice} what="invoice number" variant="ghost" />
</li>
))}
</ul>
)
}label gives the button a visible text. After a copy, the text is "Copied" for 1.5 seconds. If the label does not name the value, add what for screen readers.
import { CopyButton } from 'ferry-ui'
const INVITE_LINK = 'https://app.example.com/invite/8f2c41d9'
export default function CopyButtonLabel() {
return (
<>
<CopyButton value={INVITE_LINK} label="Copy invite link" />
<CopyButton value={INVITE_LINK} label="Copy" what="invite link" variant="ghost" />
<CopyButton value={INVITE_LINK} label="Copy" what="invite link" size="sm" />
</>
)
}CopyField selects its text when it gets the focus. The size prop is md (34px) by default, or sm (30px). Set mono to false for plain words. The field cuts a long value, but the button copies the full value.
import { CopyField } from 'ferry-ui'
export default function CopyFieldSizes() {
return (
<div className="mx-auto flex w-full max-w-md flex-col gap-3">
<CopyField value="https://api.example.com/v2" what="API URL" aria-label="API URL, medium" />
<CopyField value="https://api.example.com/v2" what="API URL" size="sm" aria-label="API URL, small" />
<CopyField value="billing@example.com" what="billing email" mono={false} aria-label="Billing email" />
<CopyField
value="https://hooks.example.com/incoming/workspace-acme/channel-announcements/a8f3e21c9b7d4e6f"
what="webhook URL"
aria-label="Webhook URL"
/>
</div>
)
}SecretField hides the value until the user reveals it. Copy takes the real value and does not reveal it. mask replaces the default dots.
The mask shows the last four characters.
import { Field, SecretField } from 'ferry-ui'
export default function CopySecret() {
return (
<div className="mx-auto flex w-full max-w-md flex-col gap-5">
<Field label="Secret key">
<SecretField value="sk_demo_4f9a2c7e1b8d3f6a" what="secret key" />
</Field>
<Field label="Recovery code" hint="The mask shows the last four characters.">
<SecretField value="4F7K-9QXM-2B8R-TL6P" what="recovery code" mask="••••-••••-••••-TL6P" />
</Field>
</div>
)
}To control the reveal state, pass revealed and onRevealedChange. Here one Switch shows or hides the two keys.
import * as React from 'react'
import { Label, SecretField, Switch } from 'ferry-ui'
export default function CopySecretControlled() {
const [shown, setShown] = React.useState(false)
return (
<div className="mx-auto flex w-full max-w-md flex-col gap-3">
<div className="flex items-center gap-2">
<Switch id="show-keys" checked={shown} onCheckedChange={setShown} />
<Label htmlFor="show-keys">Show keys</Label>
</div>
<SecretField
value="sk_demo_primary_7c1e9b4a"
what="primary key"
aria-label="Primary key"
revealed={shown}
onRevealedChange={setShown}
/>
<SecretField
value="sk_demo_secondary_2d8f3a6c"
what="secondary key"
aria-label="Secondary key"
revealed={shown}
onRevealedChange={setShown}
/>
</div>
)
}labels replaces the built-in texts of the three components. Use it to translate them.
import { CopyButton, SecretField, type CopyLabels } from 'ferry-ui'
// Module level: all the copy components of the app use this object. An entry that you leave out keeps its default.
const LABELS: Partial<CopyLabels> = {
copy: 'Duplicate',
copied: 'Done',
copyWhat: (what) => `Duplicate ${what}`,
reveal: 'Show',
hide: 'Mask',
revealWhat: (what) => `Show ${what ?? 'value'}`,
hideWhat: (what) => `Mask ${what ?? 'value'}`,
}
export default function CopyLabelsDemo() {
return (
<div className="mx-auto flex w-full max-w-md flex-col gap-3">
<div className="flex items-center gap-2">
<span className="font-mono text-[13px] text-foreground">INV-2026-0142</span>
<CopyButton value="INV-2026-0142" what="invoice number" variant="ghost" labels={LABELS} />
</div>
<SecretField value="sk_demo_4f9a2c7e1b8d3f6a" what="secret key" aria-label="Secret key" labels={LABELS} />
</div>
)
}what on an icon button. It gives the accessible name, for example "Copy API key".FormRow of Form Card, or aria-label.CopyButton also accepts the props of Button, but not onClick and children.
| Prop | Type | Default |
|---|---|---|
valueRequired | string | - |
The exact text written to the clipboard. | ||
label | string | - |
Visible label (e.g. "Copy"). When set, the button shows the label and flips to "Copied" ( | ||
what | string | - |
What is being copied ("URL", "invoice number", "API key"). Icon-only: the accessible name and tooltip become "Copy API key" — always set it there. Labelled: it is appended to the accessible name ("Copy" → "Copy API key") so several Copy buttons on one screen stay distinguishable for screen readers; omit it when the label already says what is copied ("Copy link"). | ||
onCopy | ((value: string) => void) | - |
Called with | ||
labels | Partial<CopyLabels> | - |
Overrides of the built-in texts ( | ||
variant | "link" | "default" | "primary" | "outline" | "ghost" | "destructive" | "destructive-solid" | "danger" | "danger-solid" | "warning" | "dashed" | - |
Look.
| ||
size | "tiny" | "sm" | "md" | "lg" | "icon-tiny" | "icon" | "icon-md" | "icon-lg" | - |
Height. | ||
shape | "default" | "pill" | - |
Corners: | ||
asChild | boolean | - |
Style the single child element (usually an | ||
loading | boolean | - |
Shows a spinner in place of | ||
icon | ReactNode | - |
Leading icon (rendered before children). Pass a bare lucide icon: it is sized for you. | ||
iconRight | ReactNode | - |
Trailing icon (rendered after children). Pass a bare lucide icon: it is sized for you. | ||
CopyField accepts only the props of this table.
| Prop | Type | Default |
|---|---|---|
valueRequired | string | - |
The value shown in the field and written to the clipboard. | ||
id | string | - |
Id of the input, so a | ||
mono | boolean | true |
Monospace value (default | ||
what | string | - |
What is being copied, used in the copy button's accessible name ("Copy project ID"). | ||
size | "sm" | "md" | md |
Field height: | ||
className | string | - |
Merged onto the wrapper element. | ||
aria-label | string | - |
Accessible name of the input when no | ||
aria-describedby | string | - |
Id(s) of the helper text describing the field. | ||
onCopy | ((value: string) => void) | - |
Called with | ||
labels | Partial<CopyLabels> | - |
Overrides of the built-in texts ("Copy", "Copied", and for a | ||
SecretField has the props of CopyField and the props of the reveal state.
| Prop | Type | Default |
|---|---|---|
valueRequired | string | - |
The value shown in the field and written to the clipboard. | ||
id | string | - |
Id of the input, so a | ||
mono | boolean | true |
Monospace value (default | ||
what | string | - |
What is being copied, used in the copy button's accessible name ("Copy project ID"). | ||
size | "sm" | "md" | md |
Field height: | ||
className | string | - |
Merged onto the wrapper element. | ||
aria-label | string | - |
Accessible name of the input when no | ||
aria-describedby | string | - |
Id(s) of the helper text describing the field. | ||
onCopy | ((value: string) => void) | - |
Called with | ||
labels | Partial<CopyLabels> | - |
Overrides of the built-in texts ("Copy", "Copied", and for a | ||
mask | string | - |
Text shown while hidden. Defaults to bullets roughly as long as the value (12–32). | ||
revealed | boolean | - |
Controlled reveal state. Pair with | ||
defaultRevealed | boolean | false |
Initial reveal state when uncontrolled (default | ||
onRevealedChange | ((revealed: boolean) => void) | - |
Called when the user toggles Reveal / Hide. | ||
The type of the labels object. Each entry is optional.
| Key | Default | Text |
|---|---|---|
copy | "Copy" | The Copy button of a field. |
copied | "Copied" | The confirmation after a copy. |
copyWhat(what) | "Copy API key" | The name and the tooltip of an icon button. |
reveal | "Reveal" | The tooltip of the reveal button while the mask shows. |
hide | "Hide" | The tooltip of the reveal button while the value shows. |
revealWhat(what) | "Reveal API key" | The name of the reveal button while the mask shows. |
hideWhat(what) | "Hide API key" | The name of the reveal button while the value shows. |