Inputs
API Key Field
A settings-page secret field — fixed-length mask, reveal/hide with optional auto re-mask, copy-to-clipboard with a visible failure fallback, and a two-step confirm before regenerating.
Preview in your theme
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/api-key-field.jsonPrompt
The prompt behind this component — paste it into your AI assistant to recreate or adapt it.
Build a React + TypeScript + Tailwind "ApiKeyField" component (lucide-react
Check/Copy/Eye/EyeOff/RefreshCw/TriangleAlert).
Contract
- Export a forwardRef component whose ref points at the root <div>; props:
value: string (the real secret, rendered verbatim only when revealed),
label?: string, defaultRevealed?: boolean (default false),
revealTimeout?: number (ms, default 0 = never auto-hide; > 0 re-masks
that many ms after a reveal), keepPrefix?: number (default 7),
keepSuffix?: number (default 4), onCopy?: () => void (fires only after
a successful clipboard write), onRegenerate?: () => void | Promise<void>
(called only after a two-step confirm), regenerating?: boolean (external
pending flag, OR'd with the promise-driven internal one), helper?:
ReactNode, createdAtLabel?: string (already formatted — this component
never formats dates itself), disabled?: boolean.
Behavior
- Masking: keep the first keepPrefix and last keepSuffix characters of
value, replace the middle with a FIXED number of "•" (e.g. 12) regardless
of the real string length — the mask must never leak how long the secret
actually is. If value is shorter than keepPrefix + keepSuffix, mask the
whole thing with just the dots.
- Reveal/Hide: an eye icon button (aria-pressed) toggles between the masked
string and the raw value. When revealTimeout > 0 and a reveal happens,
arm a timer that flips back to masked after revealTimeout ms; manually
hiding, re-revealing, or unmounting all clear/reset that timer — never
let two timers race.
- Copy: try navigator.clipboard.writeText(value) — on success call onCopy and
show a "Copied" line for 2s. When the Clipboard API is missing (insecure
context) or the write rejects, fall back to manual copy: reveal the key first
(arming the same auto-hide timer), then select() it on the next frame, and show
a "Copy failed — key selected, press Ctrl/Cmd+C" line. Revealing is the point —
selecting a still-masked field would tell the user to copy a row of dots.
Never fail silently.
- Regenerate (optional, only rendered when onRegenerate is passed): a
two-step destructive confirm. First click swaps the button for inline
"Confirm regenerate?" + Confirm + Cancel. Confirm calls onRegenerate();
if it returns a Promise, track an internal pending flag until it
settles (success or rejection) and OR it with the regenerating prop —
while pending, render a spinning icon + "Regenerating…" instead of the
buttons, so nothing can be double-clicked. Cancel just returns to idle
without calling onRegenerate.
- The actual <input> is a real, readOnly, font-mono <input> (not a styled
<span>) so its text is natively selectable and the browser handles
horizontal scrolling for long revealed values — never wrap, never
overflow the field.
- disabled disables every control (reveal, copy, regenerate) and dims the
field; it never blocks rendering helper/createdAtLabel text.
Rendering & styling
- Semantic tokens only: border-input / bg-transparent / focus-within:ring-ring
for the field shell, text-muted-foreground for chrome, text-destructive
for the failure line, var(--chart-2) for the copied checkmark. No
hardcoded hex/oklch.
- Field shell mimics the shadcn Input: h-9 rounded-md border px-3,
focus-within ring; icon buttons are ghost squares (hover:bg-muted,
focus-visible ring); merge consumer className via cn() onto the root.
- Two persistent live regions (never conditionally unmounted) carry the
copy status (visible, aria-live="polite") and the reveal/hide
announcement (sr-only, aria-live="polite") — persistent so every
transition actually gets announced.
Customization levers
- Mask shape: MASK_DOT_COUNT (dot count) and the keepPrefix/keepSuffix
defaults are the only knobs — keep the dot count fixed regardless of
value length, that's the whole point of the mask.
- Auto re-mask window: revealTimeout is per-instance (0 disables it); pair
a short window (2-5s) with a "just generated it, showing once" flow.
- Regenerate copy: "Confirm regenerate?" / "Regenerating…" strings and the
destructive color on Confirm are swappable without touching the
confirm/pending state machine.
- Layout: label/helper/createdAtLabel are all optional — omit any of them
to shrink the field down to just the input + action row.Concepts
- Fixed-length masking — the dot run in the middle is always the same count no matter how long the real secret is; only the declared prefix/suffix characters are ever shown, so the mask can't be used to infer key length or format.
- Reveal is reversible and self-cleaning — showing the real value is an explicit, undoable action; the optional auto re-mask timer is armed on reveal and cleared on hide/re-reveal/unmount so it never fires twice or outlives the component.
- Copy never fails silently — a successful
clipboard.writeTextgets a visible confirmation line (not just an aria-live blip); a failure selects the text as a manual fallback and says so on the page, because a settings page that "looks copied" but isn't is worse than an honest error. - Two-step confirm before a destructive regenerate —
onRegenerateonly ever fires after an explicit second click; the intermediate "Confirm regenerate?" state can still be cancelled with zero side effects. - Pending is owned by the promise, not the click — regenerating merges an internal flag (set while
onRegenerate's promise is in flight) with an externalregeneratingprop, so either the component or the caller can hold the pending state without fighting each other. - Real
<input>, not a styled<span>— using an actualreadOnlyinput gets native text selection and horizontal scrolling for free, which is why a 90-character revealed key never overflows the card.
Mention Input
A plain-text textarea where a trigger character (default @) opens a caret-anchored, filtered picker — mentions, or repurposed for slash commands.
Form Wizard
A multi-step form state machine — per-step validation gates (sync or async), focus moved to the first failing field, and a self-contained progress rail.