Skip to main content

Credit Card Input

Composite payment-card form — card number with live network detection, expiry date auto-formatting, CVC with visibility toggle, and optional cardholder name.

Quick start
import {
  CreditCardInput,
  detectNetwork,
  formatCardNumber,
  formatExpiry,
} from "mates-ui";





const cardNum    = atom("");
const expiry     = atom("");
const cvc        = atom("");
const name       = atom("");
const cvcVisible = atom(false);
const network    = atom<CardNetwork>("unknown");

CreditCardInput({
  cardNumber:     cardNum(),
  expiryDate:     expiry(),
  cvc:            cvc(),
  cardholderName: name(),
  cvcVisible:     cvcVisible(),

  onCardNumberChange: (value, net) => {
    cardNum.set(formatCardNumber(value, net));
    network.set(net);
  },
  onExpiryChange: (value) => expiry.set(formatExpiry(value)),
  onCvcChange:    (value) => cvc.set(value),
  onNameChange:   (value) => name.set(value),
  onCvcToggle:    ()      => cvcVisible.set(!cvcVisible()),
});

Basic credit card form

import { html, atom, renderApp } from 'mates';
import { CreditCardInput, formatCardNumber, formatExpiry } from 'mates-ui';

const App = () => {
const cardNum = atom("");
const expiry = atom("");
const cvc = atom("");
const ccName = atom("");
const cvcVisible = atom(false);
const network = atom("unknown");
return () => html`
<x-col gap="var(--md-space-4)" style="max-width:28rem;width:100%;">
${CreditCardInput({
cardNumber: cardNum(),
expiryDate: expiry(),
cvc: cvc(),
cardholderName: ccName(),
cvcVisible: cvcVisible(),
onCardNumberChange: (value, net) => { cardNum.set(formatCardNumber(value, net)); network.set(net); },
onExpiryChange: (value) => expiry.set(formatExpiry(value)),
onCvcChange: (value) => cvc.set(value),
onNameChange: (value) => ccName.set(value),
onCvcToggle: () => cvcVisible.set(!cvcVisible()),
})}
</x-col>
`;
};

renderApp(App, document.getElementById('app'));

Card network detection

import { html, renderApp } from 'mates';
import { detectNetwork, formatCardNumber, Text } from 'mates-ui';

const SAMPLES = [
{ label: "Visa", raw: "4242424242424242" },
{ label: "Mastercard", raw: "5425233430109903" },
{ label: "Amex", raw: "378282246310005" },
{ label: "Discover", raw: "6011111111111117" },
];

const App = () => () => html`
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--md-space-4);max-width:36rem;width:100%;">
${SAMPLES.map(({ label, raw }) => {
const net = detectNetwork(raw);
const formatted = formatCardNumber(raw, net);
return html`
<div style="padding:var(--md-space-4);border:1px solid var(--md-color-border);border-radius:var(--md-radius-md);">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--md-space-2);">
${Text(label, { type: "label-md" })}
${Text(net, { type: "label-sm", color: "muted" })}
</div>
<code style="font-size:var(--md-text-sm);font-family:monospace;">${formatted}</code>
</div>
`;
})}
</div>
`;

renderApp(App, document.getElementById('app'));

Without cardholder name

import { html, atom, renderApp } from 'mates';
import { CreditCardInput, formatCardNumber, formatExpiry } from 'mates-ui';

const App = () => {
const cardNum = atom("");
const expiry = atom("");
const cvc = atom("");
const cvcVisible = atom(false);
return () => html`
<div style="max-width:28rem;width:100%;">
${CreditCardInput({
cardNumber: cardNum(),
expiryDate: expiry(),
cvc: cvc(),
showName: false,
cvcVisible: cvcVisible(),
onCardNumberChange: (value, net) => cardNum.set(formatCardNumber(value, net)),
onExpiryChange: (value) => expiry.set(formatExpiry(value)),
onCvcChange: (value) => cvc.set(value),
onCvcToggle: () => cvcVisible.set(!cvcVisible()),
})}
</div>
`;
};

renderApp(App, document.getElementById('app'));

Disabled state

import { html, renderApp } from 'mates';
import { CreditCardInput } from 'mates-ui';

const App = () => () => html`
<div style="max-width:28rem;width:100%;">
${CreditCardInput({
cardNumber: "4242 4242 4242 4242",
expiryDate: "12/29",
cvc: "123",
cardholderName: "Jane Doe",
disabled: true,
})}
</div>
`;

renderApp(App, document.getElementById('app'));

Validation errors

import { html, atom, renderApp } from 'mates';
import { CreditCardInput, formatCardNumber, formatExpiry } from 'mates-ui';

const App = () => {
const cardNum = atom("4111 1111 111");
const expiry = atom("01/20");
const cvc = atom("1");
const name = atom("");
const cvcVisible = atom(false);
return () => html`
<div style="max-width:28rem;width:100%;">
${CreditCardInput({
cardNumber: cardNum(),
expiryDate: expiry(),
cvc: cvc(),
cardholderName: name(),
showErrors: true,
cvcVisible: cvcVisible(),
onCardNumberChange: (value, net) => cardNum.set(formatCardNumber(value, net)),
onExpiryChange: (value) => expiry.set(formatExpiry(value)),
onCvcChange: (value) => cvc.set(value),
onNameChange: (value) => name.set(value),
onCvcToggle: () => cvcVisible.set(!cvcVisible()),
})}
</div>
`;
};

renderApp(App, document.getElementById('app'));

CreditCardInput

import { CreditCardInput } from "mates-ui";
Prop Type Default Description
brandLabels Partial<Record<CardNetwork, string>> Override the aria-labels of the card-network logos (Visa, Mastercard, Amex, …) for localization.
cardholderName string Cardholder full name (shown when showName is true).
cardNumber string Card number for display — pass the formatted string (with spaces) from formatCardNumber(raw, network).
cvc string CVC / CVV digits.
cvcVisible boolean false Whether the CVC field shows plain text or is masked.
expiryDate string Expiry in "MM/YY" format (or partial while typing).
onCardNumberChange (value: string, network: CardNetwork) => void Fired on every card-number input; format the value and pass it back via cardNumber.
onCvcChange (value: string) => void Fired on every CVC input.
onCvcToggle () => void Fired when the CVC eye icon is clicked.
onExpiryChange (value: string, isValid: boolean) => void Fired on every expiry input.
onNameChange (value: string) => void Fired on every cardholder-name input.
showErrors boolean false Show inline validation error messages.
showName boolean true Show the cardholder name field.

Caveats

  • The component is stateless — you own the field values and formatting (see formatCardNumber / formatExpiry).
  • `brandLabels` affects the logo aria-labels only, e.g. { visa: "Visa-Karte" }.