InputOTP
One-time code entry with separate cells.
Installation
npx axiom add input-otppnpm dlx axiom add input-otpyarn dlx axiom add input-otpbun x axiom add input-otpThe input logic, the caret blink and the error shake live in use-input-otp.ts. Both animations are skipped when Reduce Motion is on.
Usage
import { InputOTP } from "@/components/ui/input-otp";
<InputOTP length={6} value={code} onChange={setCode} onComplete={verify} />;Props
| Prop | Type | Default | Description |
|---|---|---|---|
length | number | 6 | Number of cells. |
value | string | — | The code typed so far. |
defaultValue | string | '' | Initial code when the component manages its own value. |
onChange | (value: string) => void | — | Called on every keystroke, paste or deletion. |
onComplete | (value: string) => void | — | Called once all cells are filled. Good place to submit. |
type | 'numeric' | 'alphanumeric' | 'numeric' | Picks the keyboard and filters what can be typed or pasted. |
groups | number[] | — | Splits cells into groups with a separator, e.g. [3, 3] for 123–456. |
autoFocus | boolean | true | Opens the keyboard on the first empty cell when the screen appears. |
secure | boolean | false | Shows dots instead of digits, for a PIN. |
error | boolean | false | Red borders and a shake. Clear the value to let the user retry. |
success | boolean | false | Green cells, once the code is accepted. |
disabled | boolean | false | Blocks input, for example while the code is being verified. |
accessibilityLabel | string | 'Verification code' | What screen readers announce for the field. |
size | 'sm' | 'md' | 'md' | Cell size: 40×48 or 48×56pt. |
The field always sets textContentType="oneTimeCode", so iOS suggests the code from Messages above the keyboard, and autoComplete="sms-otp" on Android. It also accepts autoCapitalize, returnKeyType and onSubmitEditing.
Cell colors come from inputOtp.default.{default,active,invalid,success,disabled} in the component tokens.
Under the cells sits one hidden TextInput: it receives the system keyboard, paste and SMS autofill, and the cells only draw its value. This is why deleting and pasting behave like in any text field.
Use cases
Verifying a phone number
Submit in onComplete, then show the result on the cells: green on success, red on failure.
<InputOTP
length={6}
value={code}
onChange={setCode}
disabled={status === "verifying"}
error={status === "error"}
success={status === "success"}
onComplete={async (value) =>
setStatus((await verify(value)) ? "success" : "error")
}
/>Wrong code
error marks every cell. Clear the value so the next digit starts a new attempt.
<InputOTP
value={code}
onChange={(v) => {
setError(false);
setCode(v);
}}
error={error}
/>Grouped license key
type="alphanumeric" with groups for codes people copy from somewhere else. Pasting fills every cell.
<InputOTP
length={6}
groups={[3, 3]}
type="alphanumeric"
autoCapitalize="characters"
value={code}
onChange={setCode}
/>Related
- OTP verification pattern
- Passcode, which uses an in-app keypad instead of the system keyboard