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.
On this page
Installation
Usage
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.
// 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.
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.
Keeping the key private
- The key lives only in the input's
valueproperty. React would mirror avalueordefaultValueprop into thevalueattribute, 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-bwignoreanddata-privateso password managers and session replay tools leave it alone. - Nothing is logged.
onValueChangehands you the key on every edit: keep it in memory, never in analytics.
Paste cleanup
| Pasted | Becomes |
|---|---|
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
| Prop | Type | Notes |
|---|---|---|
label, description | ReactNode | Defaults to "API key" |
provider | string | Used in status copy: Checking with Anthropic |
logo | ReactNode | Your provider mark, before the label |
action | ReactNode | End of the label row, for a Create a key link |
pattern, formatHint | RegExp, string | Format check and how to fix a miss |
placeholder | string | Give an example: sk-ant-api03-… |
defaultValue | string | Written to the input's value property only |
onValueChange | (key) => void | Every edit |
onValidate | (key, { signal }) => ApiKeyValidation | Promise | boolean or { valid, message }. Throw when the check fails |
validateOnPaste | boolean | Defaults to true |
onSave | (key) => void | Promise | Shows Save key. Throw to show the error |
saved, defaultSaved | ApiKeySaved | null | The stored key, described |
onRemove | () => void | Promise | Shows Remove, behind a confirmation |
status, message | "validating" | "valid" | "invalid" | "error", ReactNode | Override the validation display |
defaultConfirmingRemove | boolean | Open the confirmation on mount |
disabled | boolean |
The root carries data-key-state: empty, typing, validating, valid, invalid, error, saving, saved or removing.
Keyboard
| Key | Action |
|---|---|
Enter | Save the key, or check it when there is no onSave. Ignored while an IME is composing |
Escape | In the remove confirmation: cancel and return focus to Replace |
Tab | Field, 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-pressedandaria-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.