Card Autoformat
inputhtmlreactvue
Live
Rendered live with React 19.2.7
card-autoformat.tsxplain css only
import './card-autoformat.css';
import { useMemo, useState } from 'react';
interface Props {
value?: string;
defaultValue?: string;
disabled?: boolean;
}
const digitsOf = (text: string): string => text.replace(/\D/g, '').slice(0, 19);
const brandOf = (digits: string): string | null => {
if (/^4/.test(digits)) return 'VISA';
if (/^(5[1-5]|2[2-7])/.test(digits)) return 'MC';
if (/^3[47]/.test(digits)) return 'AMEX';
return null;
};
const groupsFor = (brand: string | null): number[] =>
brand === 'AMEX' ? [4, 6, 5] : [4, 4, 4, 4, 3];
const format = (digits: string): string => {
const brand = brandOf(digits);
const grouped: string[] = [];
let at = 0;
for (const size of groupsFor(brand)) {
if (at >= digits.length) break;
grouped.push(digits.slice(at, at + size));
at += size;
}
return grouped.join(' ');
};
export function CardAutoformat({ value, defaultValue = '', disabled = false }: Props) {
const [internal, setInternal] = useState(format(digitsOf(defaultValue)));
const display = value ?? internal;
const raw = useMemo(() => digitsOf(display), [display]);
const brand = brandOf(raw);
const onInput = (next: string) => {
setInternal(format(digitsOf(next)));
};
return (
<span className="craf" data-brand={brand ?? undefined}>
<input
className="craf__field"
type="text"
inputMode="numeric"
autoComplete="cc-number"
placeholder="4242 4242 4242 4242"
value={display}
disabled={disabled || undefined}
aria-describedby="craf-raw"
onChange={(event) => onInput(event.target.value)}
/>
<span id="craf-raw" className="craf__raw">
{raw}
</span>
<span className="craf__brand" aria-hidden="true">
<span className="craf__card">
<span className="craf__face craf__face--blank"></span>
<span className="craf__face craf__face--brand">{brand}</span>
</span>
</span>
</span>
);
}
dependencies
react@19.2.7the dialect you are reading
Built on Astro: the stage mounts the exhibit's actual React or Vue file as a live island. Pick the framework and the stage, the code and the dependency list agree.
The formatting is the design: digits group themselves as they are typed and the brand chip flips the moment the leading digit names a network — two visible halves of one JavaScript act, which is why this study mounts live instead of posing as markup.
The raw value stays exposed structurally: the input carries the grouped display while the unformatted digits remain recoverable, because a form that mails spaces to a payment API is a formatting demo that broke checkout.