Skip to content

Copy row

A read-only value in mono with a copy button, for keys, IDs and URLs.

Status
Stable
Category
Actions
Adoption
Not used yet
import { CopyRow } from "@oration/canon/components/copy-row";
packages/canon/src/components/copy-row.tsx

Connect Okta

Paste these into a new SAML app in Okta, then come back for the metadata URL.

Entity ID

https://app.oration.ai/saml/cedarline

ACS URL

https://app.oration.ai/saml/cedarline/acs

Sign-in link

https://app.oration.ai/sso/cedarline-okta
import { Button } from "@oration/canon/components/button";import { CopyRow } from "@oration/canon/components/copy-row";import { toast } from "@oration/canon/components/toast";export function Hero() {    return (        <div className="flex w-full max-w-md flex-col gap-4 rounded-xl bg-popover p-4 text-left shadow-lg">            <div className="flex flex-col gap-1">                <p className="text-base leading-none font-medium">                    Connect Okta                </p>                <p className="text-sm text-pretty text-muted-foreground">                    Paste these into a new SAML app in Okta, then come back for                    the metadata URL.                </p>            </div>            <CopyRow                label="Entity ID"                value="https://app.oration.ai/saml/cedarline"            />            <CopyRow                label="ACS URL"                value="https://app.oration.ai/saml/cedarline/acs"            />            <CopyRow                label="Sign-in link"                value="https://app.oration.ai/sso/cedarline-okta"            />            <div className="flex justify-end gap-2">                <Button                    type="button"                    variant="ghost"                    onClick={() =>                        toast.add({ title: "Setup saved for later" })                    }                >                    Finish later                </Button>                <Button                    type="button"                    onClick={() =>                        toast.add({                            title: "Next, add the metadata URL",                            description:                                "Okta shows it on the app's Sign On tab.",                        })                    }                >                    Continue                </Button>            </div>        </div>    );}

Usage#

Copy row shows a read-only machine value with its copy button: a 12px Slate Meta label over a Well Gray well that holds the value in Geist Mono. CopyValue is the well alone, for lists where the name lives elsewhere, such as a key's row. SSO setup and the public keys list use them for entity IDs, ACS URLs and keys. The common mistake is a read-only Input for these values; it looks editable, and people click in and try to type.

When to use

  • For values people paste into another system during setup: an entity ID, an ACS URL, a sign-in link, a forwarding address.
  • For API keys and other credentials listed by name, with CopyValue beside the name.
  • In a sheet, dialog or settings card, stacked with other rows or fields.

When not to use

  • When the value should be editable. Use an input with a label. Use Input
  • For an ID inline in a header or a table cell. A bare copy button beside the text is lighter. Use Copy button
  • For a multi-line snippet or a code sample. Use Code block
  • For a label and value pair that isn't copied, such as a record's details. Use Meta line

The Tint Well Rule

Inside a card, the value sits in a Well Gray well at 70% with 10px corners, never a second bordered box.

The Machine Mono Rule

The value is a string a machine produced or will parse, so it is set in Geist Mono at 12px. The label stays in Geist Sans.

Anatomy#

ACS URL

https://app.oration.ai/saml/cedarline/acs
  1. Label. CopyRow only: 12px Slate Meta above the well, 4px gap.
  2. Well. Well Gray at 70% with 10px corners, 10px left and 4px right padding.
  3. Value. 12px Geist Mono in ink, one line, truncated with an ellipsis.
  4. Copy button. A 28px ghost Copy button. In CopyRow it is named Copy {label}.

Examples#

The well alone

CopyValue is the well without the label, for lists where the name sits elsewhere. Its label is the button's full name, and className styles the well, here dimmed for a key that's turned off.

  • ProductionActive
    pk_live_cedarline_4kT9wQ2mZx
  • StagingActive
    pk_test_cedarline_8Rb3vN1pLc
  • Legacy portalTurned off
    pk_live_cedarline_0Ah7sJ5yEd
import { CopyValue } from "@oration/canon/components/copy-row";import { Switch } from "@oration/canon/components/switch";import { toast } from "@oration/canon/components/toast";import { cn } from "@oration/canon/lib/utils";import * as React from "react";export function Values() {    const [keys, setKeys] = React.useState([        {            id: "prod",            name: "Production",            key: "pk_live_cedarline_4kT9wQ2mZx",            on: true,        },        {            id: "staging",            name: "Staging",            key: "pk_test_cedarline_8Rb3vN1pLc",            on: true,        },        {            id: "legacy",            name: "Legacy portal",            key: "pk_live_cedarline_0Ah7sJ5yEd",            on: false,        },    ]);    return (        <ul className="flex w-full max-w-lg flex-col divide-y divide-border rounded-xl bg-card text-left shadow-border">            {keys.map((key) => (                <li key={key.id} className="flex items-center gap-3 px-4 py-3">                    <div className="flex w-28 shrink-0 flex-col">                        <span className="text-13 font-medium">{key.name}</span>                        <span className="text-xs text-muted-foreground">                            {key.on ? "Active" : "Turned off"}                        </span>                    </div>                    <CopyValue                        value={key.key}                        label={`Copy ${key.name} key`}                        className={cn(                            "min-w-0 flex-1",                            !key.on && "opacity-60",                        )}                    />                    <Switch                        aria-label={`${key.name} key active`}                        checked={key.on}                        onCheckedChange={(on) => {                            setKeys((list) =>                                list.map((k) =>                                    k.id === key.id ? { ...k, on } : k,                                ),                            );                            toast.add({                                title: `${key.name} key turned ${on ? "on" : "off"}`,                            });                        }}                    />                </li>            ))}        </ul>    );}

Long values

The value stays on one line and truncates with an ellipsis. The button still copies all of it.

Forwarding address

invoices+cedarline-northwind-freight-ap@inbound.oration.ai
import { CopyRow } from "@oration/canon/components/copy-row";export function Truncation() {    return (        <div className="flex w-full max-w-[15rem] flex-col gap-3 text-left">            <CopyRow                label="Forwarding address"                value="invoices+cedarline-northwind-freight-ap@inbound.oration.ai"            />        </div>    );}

In a settings card

Rows stack with 16px between them inside a card, where the well follows the Tint Well Rule.

Email intake

Suppliers email invoices here. Each one becomes a draft for review.

Forwarding address

invoices+cedarline@inbound.oration.ai

Intake ID

int_3vX9q2Lm
import { CopyRow } from "@oration/canon/components/copy-row";export function InASheet() {    return (        <div className="flex w-full max-w-sm flex-col gap-4 rounded-xl bg-card p-4 text-left shadow-border">            <div className="flex flex-col gap-0.5">                <p className="text-sm font-medium">Email intake</p>                <p className="text-13 text-pretty text-muted-foreground">                    Suppliers email invoices here. Each one becomes a draft for                    review.                </p>            </div>            <CopyRow                label="Forwarding address"                value="invoices+cedarline@inbound.oration.ai"            />            <CopyRow label="Intake ID" value="int_3vX9q2Lm" />        </div>    );}

States#

States
StateTreatment
RestThe well, the value and a Slate Meta copy icon.
Copy hover and focusFrom Copy button: Well Gray fill on hover, the indigo focus ring.
CopiedThe icon turns into a green check for 1.6 seconds.
TruncatedA value wider than the well ends in an ellipsis; the button copies all of it.
DimmedPass opacity-60 to CopyValue for a turned-off or revoked credential, and say why beside it.

Behavior#

  • Both are stateless wrappers around Copy button; the full value goes to the clipboard whatever is visible.
  • CopyRow's button is named Copy plus label. CopyValue uses its label as the whole name, so pass Copy Production key.
  • The value is plain selectable text, so people can also select and copy part of it.
  • CopyValue fills its container's width. Size it with className, such as min-w-0 flex-1 in a row.

Do and don't#

Entity ID

https://app.oration.ai/saml/cedarline
Do. Show copyable setup values in a well with a copy button.
Don't. Put them in a read-only Input. It looks editable, and people try to type in it.

Content#

  • Labels use the other system's term exactly, so people can match fields: Entity ID, ACS URL, not Our ID.
  • Values are shown in full, never masked. If the value is a secret people shouldn't see, don't show it in a copy row.
  • Say where to paste it in the text around the rows: Paste these into a new SAML app in Okta.

Accessibility#

  • The copy button carries the name: Copy Entity ID in CopyRow, or your label in CopyValue.
  • The visible label is a <p>, not a <label>; the value isn't programmatically labelled, so screen reader users hear the value and then the named button.
  • Truncation is visual only; screen readers read the full value.
  • Copied confirmation is a name change on the button. Add a toast through Copy button when confirmation matters.
Keyboard interactions
KeysAction
TabMoves focus to the copy button.
EnterCopies the value.
SpaceCopies the value.

Design tokens#

Design tokens
TokenUsed for
--mutedWell at 70%; copy button hover
--muted-foregroundLabel and copy icon
--foregroundValue
--font-monoValue
--successCopied check

API reference#

CopyRow

A label over a CopyValue.

Props of CopyRow
PropTypeDefaultDescription
labelRequiredstringNo defaultShown above the well and used in the button's name.
valueRequiredstringNo defaultThe value shown and copied.

CopyValue

The well with the value and the copy button.

Props of CopyValue
PropTypeDefaultDescription
valueRequiredstringNo defaultThe value shown and copied.
labelRequiredstringNo defaultThe copy button's full accessible name, such as Copy Production key.
classNamestringNo defaultMerged onto the well.

Known gaps#

Where the implementation and the system disagree today. Follow the system, not the gap.

The label is a <p> with no relationship to the value, so the value has no programmatic label.

A truncated value can't be read in full anywhere on screen: no tooltip, no wrap option.

CopyRow takes no className, description or id, so it can't carry a hint or be described by an error.

It inherits Copy button's gaps: it confirms even when the clipboard write fails, and the button has no tooltip.