Skip to content
Sign in
PlaygroundAPI key inputMenu

API key input

A bring-your-own-key field: masked as prefix and last four, a reveal toggle, paste cleanup, a format check per provider, an optional live check, a saved state and a confirmed remove. The key never reaches an attribute.

Paste a key to clean it up and check it, then save, replace or remove it.

Installation

$ pnpm dlx shadcn@latest add https://chatcn.dev/r/api-key-input.json

Usage

tsx
import { ApiKeyInput, apiKeyFormats } from "@/components/ui/chatcn/api-key-input"

<ApiKeyInput
  label="Anthropic API key"
  provider="Anthropic"
  pattern={apiKeyFormats.anthropic.pattern}
  formatHint={apiKeyFormats.anthropic.hint}
  placeholder={apiKeyFormats.anthropic.placeholder}
  onValidate={(key, { signal }) => checkKey(key, signal)}
  onSave={(key) => saveKey(key)}
  saved={stored}
  onRemove={() => deleteKey()}
/>

The field checks the format on blur, paste and save, so a key with a typo never reaches the network. onValidate runs after a paste, on Enter and before saving; return true, false or { valid, message }, and throw when the check itself fails (the field then says it couldn't reach the provider, not that the key is wrong). The signal aborts when the key changes.

After onSave resolves, the input clears and the field shows the stored key as sk-ant-••••••a9F2 with when it was added. Pass saved to drive that state from your server, with prefix, last4, addedAt, lastUsedAt and addedBy. Keep the key itself server side.

tsx
// A cheap live check: list models with the key. Run it on your server, never from the browser
// with a key that is not the user's own.
async function checkKey(key: string, signal: AbortSignal) {
  const res = await fetch("/api/keys/check", { method: "POST", body: key, signal })
  if (res.status === 401) return { valid: false, message: "Anthropic rejected this key. Create a new one in the console." }
  if (!res.ok) throw new Error("check failed")
  const { models } = await res.json()
  return { valid: true, message: `${models.length} models available.` }
}

Examples

States

status and message freeze the validation display for checks you run yourself, and defaultSaved starts in the stored state.

Empty, masked, checking, key works, wrong format, rejected, check failed, saved, confirm remove and disabled.

Provider keys

chatcn bundles no provider logos. Pass your own 16 px mark through logo, with the provider's permission; the examples use plain shapes.

One field per provider with your own marks in the logo slot, the way a settings page lists them.

Keeping the key private

  • The key lives only in the input's value property. React would mirror a value or defaultValue prop into the value attribute, where extensions and replay scripts read it, so the field writes the DOM property directly and renders only the prefix and last four.
  • Saving clears the input. From then on the component holds a description of the key, never the key.
  • The input opts out of autofill, autocorrect and spellcheck, and carries data-1p-ignore, data-lpignore, data-bwignore and data-private so password managers and session replay tools leave it alone.
  • Nothing is logged. onValueChange hands you the key on every edit: keep it in memory, never in analytics.

Paste cleanup

PastedBecomes
sk-ant-api03-…a9F2⏎Trailing whitespace and line breaks removed
ANTHROPIC_API_KEY=sk-ant-…The .env name dropped, with a note under the field
export OPENAI_API_KEY="sk-proj-…"export, the name and the quotes dropped
Bearer sk-or-v1-…The Bearer prefix dropped

cleanPastedKey and describeKey are exported for your own forms. apiKeyFormats has patterns, hints and placeholders for Anthropic, OpenAI, Google AI, OpenRouter, Groq and xAI. The patterns are loose on length, so new key versions keep passing.

Props

PropTypeNotes
label, descriptionReactNodeDefaults to "API key"
providerstringUsed in status copy: Checking with Anthropic
logoReactNodeYour provider mark, before the label
actionReactNodeEnd of the label row, for a Create a key link
pattern, formatHintRegExp, stringFormat check and how to fix a miss
placeholderstringGive an example: sk-ant-api03-…
defaultValuestringWritten to the input's value property only
onValueChange(key) => voidEvery edit
onValidate(key, { signal }) => ApiKeyValidation | Promiseboolean or { valid, message }. Throw when the check fails
validateOnPastebooleanDefaults to true
onSave(key) => void | PromiseShows Save key. Throw to show the error
saved, defaultSavedApiKeySaved | nullThe stored key, described
onRemove() => void | PromiseShows Remove, behind a confirmation
status, message"validating" | "valid" | "invalid" | "error", ReactNodeOverride the validation display
defaultConfirmingRemovebooleanOpen the confirmation on mount
disabledboolean

The root carries data-key-state: empty, typing, validating, valid, invalid, error, saving, saved or removing.

Keyboard

KeyAction
EnterSave the key, or check it when there is no onSave. Ignored while an IME is composing
EscapeIn the remove confirmation: cancel and return focus to Replace
TabField, Show key, Save key

Accessibility

  • The label names the input. The description, the status line and, while masked, Key ending in a9F2 are linked with aria-describedby.
  • Show key is a toggle button with aria-pressed and aria-controls.
  • Checking, Key works and errors are announced once through a polite live region. Errors never rely on red alone: the ring, an icon and a sentence that says how to fix it.
  • The key reads left to right in every language, while the label and actions follow the page direction.
  • After saving, focus moves to Replace; after removing, back to the field; the confirmation focuses Cancel first.

Motion

  • The check mark crossfades in with the kit's 150 ms blur swap. Under reduced motion it appears.
  • A live check shows for at least 400 ms so a fast answer never flashes a spinner.
  • Reveal, mask and state changes are instant.