Input OTP
Segmented numeric code input tailored for multi-factor authentication and SMS verification codes.
Input OTP
The InputOTP component provides a clean, segmented code input specifically tailored for multi-factor authentication (MFA), two-step verification, SMS passcodes, and transaction authorization PINs.
Rather than forcing users into an awkward monolithic text box, InputOTP distributes characters across distinct tactile visual slots with automated focus shifting, backspace retreat, paste interception, and immediate onComplete triggers.
Import
import { InputOTP } from "@hashti/react";
Examples
6-Digit Authentication Code
The default configuration renders 6 distinct digit slots suitable for standard authenticator app (TOTP) codes:
import { useState } from "react";
import { InputOTP } from "@hashti/react";
export function SixDigitOTPDemo() {
const [code, setCode] = useState("");
return (
<div className="flex flex-col gap-3 max-w-xs">
<label className="text-sm font-medium">Authentication Code</label>
<InputOTP
length={6}
value={code}
onChange={setCode}
onComplete={(fullCode) => {
console.log("Submitting verification code:", fullCode);
}}
/>
<p className="text-xs text-gray-500">
Enter the 6-digit code generated by your authenticator app.
</p>
</div>
);
}
4-Digit Security PIN
Adjust the length property to render 4 slots for short transaction or access PINs:
import { useState } from "react";
import { InputOTP } from "@hashti/react";
export function FourDigitPINDemo() {
const [pin, setPin] = useState("");
return (
<div className="flex flex-col gap-3 max-w-xs">
<label className="text-sm font-medium">Vault Access PIN</label>
<InputOTP length={4} value={pin} onChange={setPin} aria-label="4-digit security PIN" />
</div>
);
}
Automated Paste Handling
InputOTP intercepts clipboard paste events across any focused slot. When a user pastes a verification code (e.g., copied from an SMS or email notification), the component strips non-alphanumeric characters, distributes the digits across all slots in sequence, advances focus to the end, and fires onComplete:
import { useState } from "react";
import { InputOTP, Button } from "@hashti/react";
export function PasteDemo() {
const [otp, setOtp] = useState("");
const [isVerifying, setIsVerifying] = useState(false);
const handleComplete = (completedOtp: string) => {
setIsVerifying(true);
setTimeout(() => {
alert(`Verified code: ${completedOtp}`);
setIsVerifying(false);
}, 800);
};
return (
<div className="flex flex-col gap-4 max-w-sm">
<div>
<label className="text-sm font-medium">SMS Verification</label>
<p className="text-xs text-gray-500 mb-3">
Paste any 6-digit code directly into the first box.
</p>
<InputOTP length={6} value={otp} onChange={setOtp} onComplete={handleComplete} />
</div>
<Button
variant="primary"
isPending={isVerifying}
isDisabled={otp.length < 6}
onPress={() => handleComplete(otp)}
>
Verify Code
</Button>
</div>
);
}
Validation Error State
Set isInvalid to display error borders when an incorrect code has been submitted:
import { useState } from "react";
import { InputOTP, Button } from "@hashti/react";
export function InvalidOTPDemo() {
const [code, setCode] = useState("123456");
const [hasError, setHasError] = useState(true);
return (
<div className="flex flex-col gap-3 max-w-xs">
<label className="text-sm font-medium">Two-Factor Code</label>
<InputOTP
length={6}
value={code}
onChange={(val) => {
setCode(val);
setHasError(false);
}}
isInvalid={hasError}
/>
{hasError && (
<span className="text-xs text-red-600 dark:text-red-400 font-medium">
The code you entered is invalid or has expired. Please try again.
</span>
)}
<Button variant="secondary" size="sm" onPress={() => setCode("")}>
Clear & Resend
</Button>
</div>
);
}
Disabled State
import { InputOTP } from "@hashti/react";
export function DisabledOTPDemo() {
return (
<InputOTP length={6} defaultValue="982341" isDisabled aria-label="Locked verification code" />
);
}
Props Reference
| Prop | Type | Default | Description |
|---|---|---|---|
length | number | 6 | Total number of individual code slots to display. |
value | string | — | The controlled string value across all slots. |
defaultValue | string | "" | The initial uncontrolled value. |
onChange | (value: string) => void | — | Callback fired whenever any digit is entered, deleted, or pasted. |
onComplete | (value: string) => void | — | Callback fired immediately when all slots have been filled. |
isDisabled | boolean | false | Disables interaction with all slot inputs. |
isInvalid | boolean | false | Applies error styling across the slot borders. |
aria-label | string | "One-time code" | Accessible name for the input group container. |
className | string | — | Additional CSS classes for the container. |
Accessibility
- Group Container Semantics: The wrapper carries
role="group"and an accessible label (aria-label="One-time code"), grouping all individual digits into a single cognitive unit for screen readers. - Per-Slot Announcements: Each underlying input is given an explicit positional label (e.g.
aria-label="Digit 1 of 6"), enabling screen reader users to understand their current position within the sequence. - Smart Keyboard Shifting:
- Entering a character automatically advances focus to the next slot.
- Pressing Backspace clears the current slot; if already empty, it clears the preceding slot and moves focus back.
- ← and → navigate between adjacent slots without altering text.
- Mobile Autofill & Virtual Keyboards: Slots use
inputMode="numeric"to summon the numeric keypad on touch devices, andautoComplete="one-time-code"to integrate with iOS and Android automated SMS code autofill features.
Keyboard Interactions
| Key | Action |
|---|---|
| Digits (0 - 9) | Fills focused slot and auto-advances to next slot. |
| Backspace | Clears current slot and moves focus back to previous slot. |
| → / ← | Navigates horizontally between slot inputs. |
WAI-ARIA & Data Attributes
| Attribute | Target | Description |
|---|---|---|
[data-focused] | OTP Slot | Set on active slot receiving input. |
[data-filled] | OTP Slot | Set when slot has a digit. |