Component
Secret field
Masked value with reveal and copy controls.
Text referenceSource and examples as JSON
npx -y github:anwarlemu/opencommerce-brand-cli#release add secret-field --surface siteRun from the target React 19.2+ app root with Tailwind CSS 4 and the @/* source alias.
API key
ac_live_••••••••••••Usage
Use for API keys or card details. Provide a descriptive label. Use defaultRevealed only when the value should initially be visible.
- Showing a secret in full by default
- Editable secrets (use an input)
- Secrets without a copy key
Exports: maskSecret, SecretText, SecretField. This component includes a client boundary. Keep event handlers and state in a client component.
Dependencies: copy-button, tooltip, lucide-react.
Example
import { SecretField } from "@/components/ui/secret-field";
export function Site() {
return (
<div className="grid max-w-md gap-2">
<p className="font-display text-caption text-fg-muted">API key</p>
<SecretField secret="ac_live_7Hq2mX9pLw4vR8sN3kTz" label="API key" />
</div>
);
}Source
"use client";
import * as React from "react";
import { Eye, EyeOff } from "lucide-react";
import { CopyButton } from "@/components/ui/copy-button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
const MASK = "••••••••••••";
export const maskSecret = (secret: string, visibleChars: number) => `${secret.slice(0, visibleChars)}${MASK}`;
/**
* The secret's text, always as wide as its masked form: revealed, the full value truncates into that same space, so
* showing it never widens the field or the table column around it.
*/
export function SecretText({ secret, masked, isRevealed, className }: { secret: string; masked: string; isRevealed: boolean; className?: string }) {
return (
<code className={cn("relative min-w-0 flex-1", className)}>
<span className={cn("block truncate", isRevealed && "invisible")} aria-hidden={isRevealed}>
{masked}
</span>
{isRevealed && <span className="absolute inset-0 truncate">{secret}</span>}
</code>
);
}
// A key or a card number in the brand's field (white, 8px corners, the 16 percent ink edge), here 40px tall to hold the
// small keys: the value masked to its first characters in the platform mono, then a show key and the Copy key.
function SecretField({
secret,
visibleChars = 8,
label = "key",
defaultRevealed = false,
className,
}: {
secret: string;
visibleChars?: number;
label?: string;
/** Starts with the full secret showing, for the one moment right after it's created. */
defaultRevealed?: boolean;
className?: string;
}) {
const [revealed, setRevealed] = React.useState(defaultRevealed);
return (
<div data-slot="secret-field" className={cn("flex h-10 min-w-0 items-center gap-1.5 rounded-control border border-hairline bg-surface pr-1.5 pl-3", className)}>
<SecretText secret={secret} masked={maskSecret(secret, visibleChars)} isRevealed={revealed} className="font-mono text-snippet text-fg" />
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => setRevealed((r) => !r)}
aria-label={revealed ? `Hide ${label}` : `Show ${label}`}
aria-pressed={revealed}
className="grid size-7 shrink-0 place-items-center rounded-sm border border-hairline bg-surface text-fg-brand outline-none transition-[background-color,scale] duration-160 ease-out hover:bg-subtle focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-focus-ring active:scale-97 motion-reduce:transition-[background-color] motion-reduce:active:scale-100 [&_svg]:size-3.5"
>
{revealed ? <EyeOff strokeWidth={1.5} /> : <Eye strokeWidth={1.5} />}
</button>
</TooltipTrigger>
<TooltipContent side="top">{revealed ? "Hide" : "Show"}</TooltipContent>
</Tooltip>
<CopyButton value={secret} label={`Copy ${label}`} />
</div>
);
}
export { SecretField };Reference: Extension from opencommerce-agentcard-web src/components/monitoring.tsx (#merchant-filter), copy-button.tsx.