@xsolla/xui-input-payment
v0.213.0
Published
A cross-platform React payment card input that automatically detects the card type from the entered number and displays relevant payment icons. <!-- BEGIN:xui-mcp-instructions:input-payment --> A specialised text input for payment data entry. Extends the
Readme
Input Payment
A cross-platform React payment card input that automatically detects the card type from the entered number and displays relevant payment icons.
A specialised text input for payment data entry. Extends the standard input with a dedicated payment icons block — a row of accepted payment method logos (Visa, Mastercard, etc.) displayed inside the field — and an optional leading icon. Used exclusively in checkout flows, billing forms, and payment method configuration screens.
When to use
- For collecting a card number, expiry date, CVV, or other payment-related values in a checkout or billing form
- When the field must visually communicate which payment methods are accepted — showing logos inside the input reassures the user before they start typing
- When the payment method icon should change dynamically based on detected card type (e.g. showing the Visa logo as soon as the user types a Visa prefix)
- As a pair: one InputPayment for the card number, one for the expiry, one for the CVV — in a standard payment card form layout
When not to use
- For non-payment text inputs — use the standard Input component
- When no payment branding is needed — use standard Input with an optional left icon
- When accepting only one specific payment method with no ambiguity — the payment icons block may be omitted and a single logo shown via Icon left instead
Content guidelines
- Placeholder text — use format hints, not instructions: 1234 5678 9012 3456 for card number, MM / YY for expiry, CVV or CVC for security code. Do not use "Enter your card number" — the field label already communicates this.
- Error messages — be specific:
- "Invalid card number" — after Luhn check fails
- "Expiry date has passed" — past month/year entered
- "CVV must be 3 digits" (or 4 for Amex) — wrong length
- "Card number is required" — required field left empty
- Field labels — always provide a visible label above the field: "Card number", "Expiry date", "Security code". Do not rely on the placeholder alone.
- Payment icons — show only the logos for payment methods actually accepted by the product's payment provider. Do not show logos for unsupported methods — it creates false expectations.
Behaviour guidelines
- Card number formatting — as the user types, format the card number with spaces every 4 digits: 4242 4242 4242 4242. Apply this formatting in real time without disrupting cursor position. Accept both numeric-only input and formatted strings with spaces.
- Card type detection — detect the card network from the first 1–6 digits (IIN/BIN range). When detected, highlight the matching logo in the payment icons block and dim or hide others. Update in real time as the user types.
- Expiry date formatting — auto-insert a / separator after the month digits: 12/26. Prevent the user from entering an invalid month (> 12) or a past expiry date.
- CVV masking — optionally mask CVV input as the user types (show dots •••). Provide a show/hide toggle via a trailing icon button if masking is enabled.
- Input masking — use inputmode="numeric" and pattern="[0-9]"* on numeric fields to trigger a numeric keyboard on mobile. Do not use type="number" for card fields — it interferes with leading zeros and formatting.
- Validation timing — validate on blur (when the user leaves the field), not on every keystroke. Switch to State=Error with a specific error message when the value fails validation. Clear the error when the user starts typing again.
- Disabled state — use State=Disabled for saved payment methods being displayed in a non-editable summary. Always show the masked card value (e.g. •••• 4242) rather than an empty disabled field.
- Autofill — support browser and OS autofill for payment fields. Use standard autocomplete attributes: autocomplete="cc-number" for card number, autocomplete="cc-exp" for expiry, autocomplete="cc-csc" for CVV. Autofill should trigger Filled=True and the detected card type should update the payment icons block.
Accessibility
- Each InputPayment field must have a visible label associated via or aria-labelledby. Do not use placeholder as the only label.
- Use autocomplete attributes on all payment fields — they are required for WCAG 1.3.5 (Identify Input Purpose).
- Use inputmode="numeric" on card number, expiry, and CVV fields to trigger the numeric keyboard on touch devices.
- When State=Error, the error message must be associated via aria-describedby so screen readers announce it when the field is focused.
- The payment icons block is decorative — wrap it in aria-hidden="true" so screen readers do not attempt to read logo names. The accepted payment methods should instead be listed in a visible or screen-reader-only text near the form (e.g. "We accept Visa, Mastercard, and American Express").
- The Icon left is decorative when a label is present — set aria-hidden="true" on the icon element.
- When State=Disabled, set aria-disabled="true" and communicate the card type and masked number via aria-label — e.g. aria-label="Visa card ending in 4242, disabled".
Installation
npm install @xsolla/xui-input-paymentDemo
Basic Payment Input
import * as React from "react";
import { InputPayment } from "@xsolla/xui-input-payment";
export default function BasicPayment() {
const [cardNumber, setCardNumber] = React.useState("");
return (
<InputPayment
value={cardNumber}
onChangeText={setCardNumber}
placeholder="Card number"
/>
);
}With Auto-Detection Callback
import * as React from "react";
import { InputPayment } from "@xsolla/xui-input-payment";
export default function WithDetection() {
const [cardNumber, setCardNumber] = React.useState("");
const [cardType, setCardType] = React.useState<string | null>(null);
return (
<div>
<InputPayment
value={cardNumber}
onChangeText={setCardNumber}
onRecognizedPaymentChange={(type) => setCardType(type)}
/>
{cardType && <p>Detected: {cardType}</p>}
</div>
);
}Different Sizes
import * as React from "react";
import { InputPayment } from "@xsolla/xui-input-payment";
export default function Sizes() {
return (
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
<InputPayment size="sm" placeholder="Small" />
<InputPayment size="md" placeholder="Medium" />
<InputPayment size="lg" placeholder="Large" />
</div>
);
}Anatomy
import { InputPayment } from "@xsolla/xui-input-payment";
<InputPayment
value={cardNumber} // Card number value
onChangeText={setCardNumber} // Change handler
size="md" // Input size
placeholder="Card number" // Placeholder text
possiblePayments={["visa", "mastercard"]} // Accepted cards
maxVisiblePossiblePayments={5} // Max icons shown
recognizedPayment="visa" // Force recognized type
autoDetect={true} // Enable auto-detection
errorMessage="Invalid card" // Error message
disabled={false} // Disabled state
/>;Examples
Custom Accepted Cards
import * as React from "react";
import { InputPayment } from "@xsolla/xui-input-payment";
export default function CustomCards() {
return (
<InputPayment
possiblePayments={["visa", "mastercard", "amex"]}
maxVisiblePossiblePayments={3}
placeholder="We accept Visa, Mastercard, Amex"
/>
);
}With Error State
import * as React from "react";
import { InputPayment } from "@xsolla/xui-input-payment";
export default function WithError() {
const [cardNumber, setCardNumber] = React.useState("");
const [error, setError] = React.useState("");
const validate = (value: string) => {
if (value.length > 0 && value.length < 13) {
setError("Card number too short");
} else {
setError("");
}
};
return (
<InputPayment
value={cardNumber}
onChangeText={(text) => {
setCardNumber(text);
validate(text);
}}
errorMessage={error}
/>
);
}Controlled Recognition
import * as React from "react";
import { InputPayment } from "@xsolla/xui-input-payment";
export default function ControlledRecognition() {
const [cardNumber, setCardNumber] = React.useState("");
return (
<InputPayment
value={cardNumber}
onChangeText={setCardNumber}
autoDetect={false}
recognizedPayment={cardNumber.startsWith("4") ? "visa" : undefined}
/>
);
}With Icon
import * as React from "react";
import { InputPayment } from "@xsolla/xui-input-payment";
import { CreditCard } from "@xsolla/xui-icons-base";
export default function WithIcon() {
return <InputPayment icon={<CreditCard />} placeholder="Enter card number" />;
}In Payment Form
import * as React from "react";
import { InputPayment } from "@xsolla/xui-input-payment";
import { Input } from "@xsolla/xui-input";
import { Button } from "@xsolla/xui-button";
export default function PaymentForm() {
const [cardNumber, setCardNumber] = React.useState("");
const [cardType, setCardType] = React.useState<string | null>(null);
return (
<form
style={{
display: "flex",
flexDirection: "column",
gap: 16,
maxWidth: 400,
}}
>
<InputPayment
value={cardNumber}
onChangeText={setCardNumber}
onRecognizedPaymentChange={setCardType}
placeholder="Card number"
/>
<div style={{ display: "flex", gap: 16 }}>
<Input placeholder="MM/YY" style={{ flex: 1 }} />
<Input placeholder="CVV" style={{ width: 100 }} />
</div>
<Input placeholder="Cardholder name" />
<Button onPress={() => console.log("Submit", { cardNumber, cardType })}>
Pay Now
</Button>
</form>
);
}API Reference
InputPayment
InputPaymentProps:
| 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 | - | Card number value. |
| onChange | (e: ChangeEvent) => void | - | Standard change event handler. |
| onChangeText | (text: string) => void | - | Text change handler. |
| size | "xs" \| "sm" \| "md" \| "lg" \| "xl" | "md" | Input size variant. |
| placeholder | string | "Card number" | Placeholder text. |
| icon | ReactNode | - | Left icon. |
| disabled | boolean | false | Disabled state. |
| error | boolean | - | Error state indicator. |
| errorMessage | string | - | Error message text. |
| possiblePayments | PaymentSystemKey[] | See below | Accepted payment types. |
| maxVisiblePossiblePayments | number | 5 | Max payment icons shown. |
| recognizedPayment | PaymentSystemKey | - | Force recognized payment type. |
| onRecognizedPaymentChange | (type: PaymentSystemKey \| null) => void | - | Detection callback. |
| autoDetect | boolean | true | Enable auto-detection. |
| aria-label | string | "Card number" | Accessible label. |
| testID | string | - | Test identifier. |
Default possiblePayments:
[
"mastercard",
"visa",
"maestro",
"diners",
"amex",
"discover",
"jcb",
"unionpay",
];PaymentSystemKey:
type PaymentSystemKey =
| "visa"
| "mastercard"
| "amex"
| "diners"
| "maestro"
| "unionpay"
| "discover"
| "jcb"
| "aura"
| "cartesbancaires"
| "cirrus"
| "dankort"
| "elo"
| "hipercard"
| "mir"
| "naranja"
| "paypal"
| "sodexo"
| "uatp";Card Detection
The component automatically detects card types based on BIN (Bank Identification Number) ranges:
| Card Type | BIN Pattern | | :--------------- | :------------------------- | | Visa | Starts with 4 | | Mastercard | 51-55 or 2221-2720 | | American Express | 34, 37 | | Discover | 6011, 644-649, 65 | | JCB | 3528-3589 | | Diners Club | 300-305, 36, 38 | | UnionPay | 62 (except Discover range) | | Maestro | 50, 56-69 | | Mir | 2200-2204 |
Icon Animation
- Payment icons cycle through when multiple are available
- When a card type is detected, other icons slide out
- The recognized card icon remains visible
- Animation is smooth with 300ms transitions
Accessibility
- Input has
aria-labelfor screen readers - Error messages are linked via
aria-describedby - Payment icons have descriptive
aria-label - Disabled state is announced with
aria-disabled inputMode="numeric"shows numeric keyboard on mobile
