Skip to content
Settings
Theme
Mode

Card Autoformat

inputhtmlreactvue

Live

4242424242424242

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.

v0.19.0