Card Input
Card number, expiry and CVC in one control, with brand detection and a Luhn check. Client-side validation catches the typo; it never claims the card is good.
src/components/ui/card-input.tsxAbout
From the component's own source, where it was written.
Card number, expiry and CVC as one control.
The number is validated with the Luhn checksum, which catches a mistyped digit before the request is made. That matters commercially, not just for polish: failed authorisations count against a merchant's decline rate, and card networks price on it.
The brand is detected from the leading digits so the CVC length can follow — American Express uses four, everyone else three, and a fixed maxlength of three silently truncates an Amex code.
Nothing here stores or transmits anything. In production the fields belong in a PSP iframe so the page never touches a PAN; this is the shape for a demo, a test harness, or a PSP that hands you styling control.
Install
npx astralyx-ui add card-input
Usage
import { CardInput } from '@/components/ui/card-input'
<CardInput onCardChange={(card) => setCard(card)} />Composer
Change the props and the source below follows.
<CardInput onCardChange={setCard} />Examples
Default and small
<CardInput onCardChange={setCard} />API
Generated from the source, so it cannot fall behind the code. One table per exported component, then hooks, functions and types.
CardInput
Extends Omit<ComponentProps<'div'>, 'onChange'>.
| Prop | Type | Default | Description |
|---|---|---|---|
| onCardChange | (card: { number: string expiry: string cvc: string brand: CardBrand valid: boolean }) => void | — | Fires with { number, expiry, cvc, brand, valid } on every keystroke. valid means the digits pass Luhn and the expiry is in the future — not that the card will authorise. |
| size | keyof typeof fieldSize | 'md' | — |
| numberLabel | string | 'Card number' | Accessible name for the number field. |
| expiryLabel | string | 'Expiry date' | — |
| expiryPlaceholder | string | 'MM/YY' | — |
| cvcLabel | string | 'Security code' | — |
| invalidNote | ReactNode | 'That card number does not check out — one of the digits is wrong.' | Shown when the digits fail the Luhn check. |
| disabled | boolean | false | — |
| security | none stored | — | The component holds the number in local state only, and never in a named field that a form serialiser would sweep up. |
Functions
| Function | Signature | Description |
|---|---|---|
| detectBrand | (digits: string) => CardBrand | Brand from the issuer identification number. |
| luhnValid | (digits: string) => boolean | Luhn checksum — catches a single mistyped digit and most transpositions. |
Types
| Type | Definition | Description |
|---|---|---|
CardBrand | 'visa' | 'mastercard' | 'amex' | 'discover' | 'unknown' | Card number, expiry and CVC as one control. |
Behaviour
| Topic | Detail | Description |
|---|---|---|
| brand detection | automatic | From the issuer identification number, which also sets the expected length and the CVC length: Amex is 15 digits with a 4-digit code. |
| focus | automatic | Advances to expiry and then CVC as each segment fills, and steps back on backspace from an empty field. |