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…

"use client"

import * as React from "react"
import { Check, Copy, Eye, EyeOff, RefreshCw, TriangleAlert } from "lucide-react"
import { cn } from "@/lib/utils"

/** The masked middle is always this many dots — fixed length, so the real key length never leaks. */
const MASK_DOT_COUNT = 12
/** How long the "Copied" / "Copy failed" line stays visible before resetting to idle. */
const COPY_STATUS_DELAY = 2000

export interface ApiKeyFieldProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onCopy" | "children"> {
  /** The real secret. Rendered verbatim once revealed — this component never reformats it. */
  value: string

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/api-key-field.json

Prompt

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.writeText gets 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 regenerateonRegenerate only 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 external regenerating prop, so either the component or the caller can hold the pending state without fighting each other.
  • Real <input>, not a styled <span> — using an actual readOnly input gets native text selection and horizontal scrolling for free, which is why a 90-character revealed key never overflows the card.

On This Page