@xsolla/xui-input-pin
v0.199.0
Published
A cross-platform React PIN/OTP input component with individual cells for each digit. Features auto-advance between cells, paste support, and completion callbacks. <!-- BEGIN:xui-mcp-instructions:input-pin --> A specialised input for entering a short numer
Readme
Input Pin
A cross-platform React PIN/OTP input component with individual cells for each digit. Features auto-advance between cells, paste support, and completion callbacks.
A specialised input for entering a short numeric or alphanumeric code — typically a PIN, OTP (one-time password), or verification code. Renders as a horizontal row of individual character cells (.pin-item), one per expected digit. Each cell has independent state, and the component supports masking, placeholder dots, and a visibility toggle.
When to use
- For entering a PIN, OTP, SMS verification code, or 2FA code
- When a fixed-length code must be collected and each character should be visually distinct and finger-friendly
- In authentication flows, payment confirmation screens, and account verification modals
- When real-time digit-by-digit visual feedback improves the entry experience
When not to use
- For variable-length passwords — use InputPassword
- For long codes (more than 8 characters) that do not benefit from individual cell rendering — use a standard Input with masking
- For alphanumeric codes where the user must type non-numeric characters (letters) that are already clearly visible — use a styled Input instead of a PIN field
- When the code length is unknown ahead of time — InputPin requires a fixed number of cells
Behaviour guidelines
Auto-advance — after the user types a digit into the active cell, focus automatically moves to the next empty cell. The user should never need to manually click individual cells.
Auto-submit — when the last cell receives a value and all cells are filled (State=Filled), submit the form automatically without requiring the user to press Enter or a Submit button. This is the expected UX for OTP and PIN flows.
Backspace — pressing Backspace clears the value in the current active cell and moves focus back to the previous cell. If the active cell is already empty, Backspace moves focus back and clears the previous cell.
Paste — when the user pastes a string of the correct length, distribute the characters across all cells immediately and submit if auto-submit is enabled. Trim whitespace and hyphens from pasted values (e.g. 1234 5678 → 12345678).
Numeric-only enforcement — for numeric PINs/OTPs, reject non-numeric characters silently (do not show an error for a single wrong keypress — just ignore it). Use inputmode="numeric" to trigger a numeric keyboard on mobile.
Error state — switch to State=Error when the code fails server-side validation (wrong OTP, expired code, incorrect PIN). All cells switch to error appearance simultaneously. Show a specific error message below the component. Clear the error state when the user starts typing again and reset all cells.
OTP code expiry — for time-based OTPs, show a countdown timer or "Resend code" link near the component. If the code expires mid-entry, show State=Error with the message "Code has expired. Request a new one."
Resend flow — after an error due to expiry or incorrect code, provide a "Resend code" action that clears all cells, resets to State=Default, and moves focus to the first cell.
Disabled state — use State=Disable when the PIN entry is temporarily locked (e.g. too many failed attempts). Show a reason nearby (e.g. "Too many attempts. Try again in 30 seconds.").
Mobile keyboard — use inputmode="numeric" and pattern="[0-9]"* on the underlying input. Consider rendering a single hidden text input that captures keystrokes and distributes characters to the visual cells, rather than using multiple separate elements.
Content guidelines
Label — always provide a visible label above the component: "Enter PIN", "Verification code", "Enter the 6-digit code sent to +7 (900) •••• 1234". Include the code length and delivery channel in the label for OTP flows. Error messages — be specific and actionable:
- "Incorrect PIN. 2 attempts remaining."
- "Code expired. Request a new code below."
- "Incorrect code. Please try again."
- Resend link label — use "Resend code" or "Send a new code". Include a cooldown timer: "Resend code in 0:45".
- Placeholder dot vs empty — prefer Placeholder-dots=True for PIN entry screens where the digit count helps the user count positions. Use Placeholder-dots=False for cleaner designs where the code length is communicated in the label.
Accessibility
The underlying input mechanism must be accessible via keyboard. The preferred implementation is a single visually-hidden element that captures keyboard input and distributes it to the visual cells — this avoids focus-management complexity across multiple inputs.
If multiple elements are used (one per cell), each must have aria-label identifying the position: e.g. aria-label="Digit 1 of 6".
The component container must have aria-label describing the whole field: e.g. aria-label="6-digit verification code".
When State=Error, the error message must be in an aria-live="polite" region and associated via aria-describedby so screen readers announce it.
The visual dot masking is cosmetic — the underlying must still use type="password" (or inputmode="numeric" on a text input with custom masking) to ensure the value is treated as sensitive.
Auto-advance (moving focus to the next cell) must also move focus for keyboard users, not just update visual state.
When the last cell is filled and the form is auto-submitted, announce the submission to screen readers via aria-live="assertive" or by focussing a confirmation message.
Ensure a minimum touch target of 44 × 44 px on mobile. For XS [32] and S [40] sizes, add layout padding to meet this requirement.
Installation
npm install @xsolla/xui-input-pinDemo
Basic PIN Input
import * as React from "react";
import { InputPin } from "@xsolla/xui-input-pin";
export default function BasicPinInput() {
const handleComplete = ({ value, isComplete }) => {
if (isComplete) {
console.log("PIN entered:", value);
}
};
return <InputPin codeLength={4} onComplete={handleComplete} />;
}Six-Digit Code
import * as React from "react";
import { InputPin } from "@xsolla/xui-input-pin";
export default function SixDigitPin() {
return (
<InputPin
codeLength={6}
label="Enter verification code"
onComplete={({ value }) => console.log("Code:", value)}
/>
);
}Controlled PIN Input
import * as React from "react";
import { InputPin } from "@xsolla/xui-input-pin";
export default function ControlledPinInput() {
const [pin, setPin] = React.useState("");
return (
<div>
<InputPin
value={pin}
codeLength={4}
onChange={({ value }) => setPin(value)}
/>
<p>Current value: {pin}</p>
</div>
);
}Secure Entry
import * as React from "react";
import { InputPin } from "@xsolla/xui-input-pin";
export default function SecurePinInput() {
return (
<InputPin codeLength={4} secureTextEntry={true} label="Enter your PIN" />
);
}Anatomy
import { InputPin } from "@xsolla/xui-input-pin";
<InputPin
value={pinValue} // Controlled value
onChange={handleChange} // Change handler with {value, isComplete}
onComplete={handleComplete} // Called when all digits entered
codeLength={4} // Number of digits
size="md" // Size variant
label="Label" // Label above input
secureTextEntry={false} // Hide entered digits
showPlaceholderDots={true} // Show dot placeholders
flexibleWidth={false} // Expand cells to fill width
disabled={false} // Disabled state
error={false} // Error state
errorMessage="Error" // Error message text
/>;Examples
Full Width
import * as React from "react";
import { InputPin } from "@xsolla/xui-input-pin";
export default function FullWidthPin() {
return (
<div style={{ width: 300 }}>
<InputPin codeLength={6} flexibleWidth={true} label="Verification Code" />
</div>
);
}With Error
import * as React from "react";
import { InputPin } from "@xsolla/xui-input-pin";
export default function ErrorPinInput() {
return (
<InputPin
codeLength={4}
error={true}
errorMessage="Invalid PIN. Please try again."
label="Enter PIN"
/>
);
}PIN Input Sizes
import * as React from "react";
import { InputPin } from "@xsolla/xui-input-pin";
export default function PinInputSizes() {
return (
<div style={{ display: "flex", flexDirection: "column", gap: 24 }}>
<InputPin size="xs" codeLength={4} />
<InputPin size="sm" codeLength={4} />
<InputPin size="md" codeLength={4} />
<InputPin size="lg" codeLength={4} />
<InputPin size="xl" codeLength={4} />
</div>
);
}API Reference
InputPin
InputPin Props:
| Prop | Type | Default | Description |
| :------------------ | :----------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------ |
| testID | string | — | Test ID for testing frameworks. On web this renders as data-testid; on React Native it renders as testID. |
| value | string | "" | Current PIN value. |
| onChange | (props: OnInputPinCompleteProps) => void | - | Called on every change. |
| onComplete | (props: OnInputPinCompleteProps) => void | - | Called when all digits entered. |
| codeLength | number | 4 | Number of PIN digits. |
| size | "xl" \| "lg" \| "md" \| "sm" \| "xs" | "md" | Component size. |
| label | string | - | Label above input. |
| secureTextEntry | boolean | false | Hide entered digits. |
| showPlaceholderDots | boolean | true | Show dot placeholders. |
| flexibleWidth | boolean | false | Expand cells to fill container. |
| disabled | boolean | false | Disabled state. |
| error | boolean | false | Error state. |
| errorMessage | string | - | Error message text. |
| testID | string | - | Test identifier. |
| aria-label | string | - | Accessible label for screen readers. |
OnInputPinCompleteProps:
interface OnInputPinCompleteProps {
isComplete: boolean; // Whether all digits are filled
value: string; // Current PIN value
}Keyboard Navigation
| Key | Action | | :----------- | :------------------------------- | | 0-9, A-Z | Enter character and advance | | Backspace | Clear current cell and move back | | Arrow Left | Move to previous cell | | Arrow Right | Move to next cell | | Ctrl/Cmd + V | Paste and auto-fill cells |
Behavior
- Auto-advances to next cell after input
- Supports paste to fill multiple cells
- Backspace clears current or moves to previous
- Only alphanumeric characters are accepted
- Uses
inputMode="numeric"for mobile keyboards
Accessibility
- Each cell has
aria-labelindicating digit position role="group"on container witharia-labelledbyaria-invalidwhen in error state- Error messages linked via
aria-describedby
