formatting, card type, and Luhn checksum working together
A credit card input mask is more than inserting a space every four digits. Card type detection, different grouping lengths for American Express, the Luhn checksum, and a cursor that does not constantly jump back while typing turn a simple formatting task into a small but demanding Alpine.js project.
Table of contents
- 1. Why a credit card input mask is more than formatting
- 2. Detecting card type live: Visa, Mastercard, American Express
- 3. Formatting digits into blocks of four, with the Amex special case
- 4. Not losing cursor position while formatting
- 5. The Luhn algorithm: a checksum without a server request
- 6. Expiry date and check digit with matching rules
- 7. Accessible error messages and autofill compatibility
- 8. Security boundaries: what the client mask must not do
- 9. Input mask approaches compared
- 10. Summary
- 11. FAQ
1. Why a credit card input mask is more than formatting
An unformatted sequence of sixteen digits is hard for humans to read and even harder to check for typos. A good credit card input mask visually groups the digits, detects the card type from the first few digits, and gives immediate feedback on whether the entered number can even be structurally valid, long before a payment is actually authorized.
The naive approach of inserting a space after every fourth digit on every keystroke only works for card types with uniform grouping into fours. American Express, however, uses a 4-6-5 pattern, which means a credit card input mask must know the detected card type before it even decides where to insert spaces.
The third building block is the Luhn checksum, a simple mathematical algorithm that virtually all common credit card numbers satisfy. Together, card type detection, correct grouping, and the Luhn check produce a credit card input mask that feels like a professional payment solution, even though it runs entirely client side with Alpine.js.
2. Detecting card type live: Visa, Mastercard, American Express
Every card issuer uses a fixed prefix pattern for its card numbers: Visa always starts with a 4, Mastercard with 51 to 55 or the newer range 2221 to 2720, American Express with 34 or 37. These prefixes can be matched with simple regular expressions as soon as the first one to four digits have been entered.
The credit card input mask should not only store the detected type internally, but also reflect it visually, for instance through a small logo icon that appears while typing. This gives the user immediate confidence that the input is being interpreted correctly, and allows the mask to apply the correct grouping rule from that point on.
// Card type detection based on IIN (Issuer Identification Number) prefixes
function creditCardField() {
return {
rawNumber: '',
cardType: null,
detectCardType(digits) {
if (/^4/.test(digits)) return 'visa';
if (/^5[1-5]/.test(digits) || /^2(22[1-9]|2[3-9]|[3-6]|7[0-1]|720)/.test(digits)) return 'mastercard';
if (/^3[47]/.test(digits)) return 'amex';
return null;
},
onInput(event) {
const digits = event.target.value.replace(/\D/g, '');
this.rawNumber = digits;
this.cardType = this.detectCardType(digits);
},
};
}
3. Formatting digits into blocks of four, with the Amex special case
Once the card type is known, the credit card input mask decides on the matching grouping pattern. Visa and Mastercard follow the simple 4-4-4-4 pattern for sixteen digits. American Express, however, uses fifteen digits in a 4-6-5 pattern, which means a generic blocks-of-four formatter would produce incorrectly formatted and thus confusing output for Amex cards.
The formatting function should therefore accept an array of group sizes that differs depending on the detected card type. This separation of raw data and display format is important: the internal state always stores only the plain digits, while formatting is applied only at render time.
function creditCardField() {
return {
rawNumber: '',
cardType: null,
// Group sizes differ per card type: Amex uses 4-6-5, others use 4-4-4-4
groupSizes: {
visa: [4, 4, 4, 4],
mastercard: [4, 4, 4, 4],
amex: [4, 6, 5],
default: [4, 4, 4, 4],
},
get formatted() {
const sizes = this.groupSizes[this.cardType] ?? this.groupSizes.default;
const groups = [];
let position = 0;
for (const size of sizes) {
const chunk = this.rawNumber.slice(position, position + size);
if (!chunk) break;
groups.push(chunk);
position += size;
}
return groups.join(' ');
},
};
}
4. Not losing cursor position while formatting
As soon as spaces are inserted into a text field programmatically, the cursor jumps to the end of the field in most naive implementations. The user then types in the middle of the card number, but sees the cursor hop to the end of the credit card input mask after every input, which makes correcting individual digits practically impossible.
The solution is to remember the cursor position before reformatting, count the number of newly inserted spaces up to the original position, and then set the cursor to the corrected position. This requires direct access to selectionStart and setSelectionRange() of the input element, which in Alpine happens via a method on the $refs object.
function creditCardField() {
return {
rawNumber: '',
onInput(event) {
const input = event.target;
const previousLength = input.value.length;
const cursorPosition = input.selectionStart;
this.rawNumber = input.value.replace(/\D/g, '').slice(0, 16);
const newValue = this.formatted;
this.$nextTick(() => {
input.value = newValue;
// Adjust cursor for inserted spaces before the original position
const lengthDiff = newValue.length - previousLength;
const newPosition = Math.max(0, cursorPosition + lengthDiff);
input.setSelectionRange(newPosition, newPosition);
});
},
};
}
5. The Luhn algorithm: a checksum without a server request
The Luhn algorithm is a simple checksum formula that virtually all common credit card numbers satisfy. It doubles every second digit from the right, subtracts nine if the result is above nine, sums all digits, and checks whether the sum is divisible by ten. This check reliably catches single digit typos and most transpositions of adjacent digits, without needing a network request.
It is important that a valid Luhn checksum only means the number is structurally plausible, not that the card actually exists or is funded. The credit card input mask uses Luhn exclusively to catch obvious typos early, while the actual authorization always takes place at the payment service provider.
function creditCardField() {
return {
rawNumber: '',
// Luhn checksum: catches typos before any network request
isLuhnValid() {
const digits = this.rawNumber.split('').map(Number).reverse();
let sum = 0;
digits.forEach((digit, index) => {
if (index % 2 === 1) {
digit *= 2;
if (digit > 9) digit -= 9;
}
sum += digit;
});
return this.rawNumber.length >= 13 && sum % 10 === 0;
},
};
}
6. Expiry date and check digit with matching rules
Besides the card number, every credit card input mask also includes the expiry date in MM/YY format and the CVC code on the back of the card. The expiry date field should automatically insert a slash after the first two digits and simultaneously check whether the month is between 01 and 12 and the date lies in the future, not already expired.
The length of the CVC code also depends on the card type: American Express uses four digits, all other common issuers three. The credit card input mask should therefore dynamically adjust the maximum input length of the CVC field to the previously detected card type, instead of enforcing a fixed length for all cards.
7. Accessible error messages and autofill compatibility
Browser autofill often fills credit card fields without spaces or with different formatting. A robust credit card input mask must therefore run through the same cleanup and reformatting on the @input event after an autofill as it does on manual input, otherwise the field shows an inconsistent display after autofill.
For screen reader users, the card number field should carry an autocomplete="cc-number" attribute, the expiry date autocomplete="cc-exp", and the CVC autocomplete="cc-csc". These standard attributes not only enable password manager integration, but also allow assistive technology to correctly announce the purpose of each field. An invalid Luhn result should be communicated via aria-invalid and a clear text message, not only through a red border.
8. Security boundaries: what the client mask must not do
A credit card input mask in Alpine.js must never be the sole security boundary of a payment. PCI DSS requirements demand that raw card numbers never reach your own server in most configurations, but are instead transmitted directly to a certified payment service provider like Stripe or Adyen, usually via an embedded iFrame form field provided by that vendor.
The mask described here is therefore primarily suited for guiding the user: formatting, card type display, and early error detection. As soon as a PCI compliant payment provider is involved, its own isolated form field takes over the actual entry of sensitive data, while the Alpine component is only responsible for the surrounding form logic.
9. Input mask approaches compared
The table below compares typical pitfalls when building a credit card input mask with the recommended approach.
| Aspect | Error prone | Recommended input mask | Benefit |
|---|---|---|---|
| Grouping | fixed 4-4-4-4 for all cards | group size per card type | correct for Amex 4-6-5 |
| Cursor | jumps to the end on every input | correct position via length diff | mid-field correction remains possible |
| Plausibility | length check only | Luhn checksum | catches typos early |
| CVC length | fixed three digits for all | four digits for Amex, three otherwise | correct for all card types |
| Storage | raw number to your own server | PCI compliant payment provider | no PCI DSS obligations for you |
A well built credit card input mask significantly improves user guidance, but never replaces the actual security architecture of a payment integration. Both layers complement each other when kept clearly separate.
Mironsoft
Alpine.js payment forms and PCI compliant checkout integration
Payment form that feels unprofessional?
We build you a credit card input mask with card type detection, Luhn checking, and clean integration into PCI compliant payment providers for your Magento checkout.
Input mask
Custom formatting, card type detection, and Luhn checking
Payment provider integration
Connecting Stripe, Adyen, and other PCI compliant providers
Accessibility
Autocomplete attributes and aria-invalid for payment fields
10. Summary
A good credit card input mask in Alpine.js combines four building blocks: card type detection based on the prefix, card type dependent grouping, cursor correction that still allows edits in the middle of the number, and the Luhn checksum as an early plausibility check without a server request. Expiry date and CVC length follow the same card type dependent rules.
What remains important is the clear boundary: the mask improves usability, but never replaces the PCI compliant transmission of sensitive card data to a certified payment service provider. Whoever maintains this separation from the start gets a credit card input mask that feels professional while still respecting the payment industry's security requirements.
Credit Card Input Mask with Alpine.js — The Essentials at a Glance
Card type
Prefix detection for Visa, Mastercard, and Amex drives grouping and CVC length.
Cursor
Correct position after reformatting via the length difference, do not jump to the end.
Luhn
Checksum catches typos client side, but does not replace real authorization.
Security
Raw card numbers belong in a PCI compliant provider form, not on your own server.