# React credit card form

_Box Kite 2.2.4 · a markdown copy of https://www.box-kite.dev/blocks/react-credit-card-form/_

A React credit card form: number, name, expiry and security code, formatted as typed, checked on blur, with a card that fills in above.

## Install

One command copies the files into your project, as code you then own. It needs a `components.json`, and [the shadcn registry page](https://www.box-kite.dev/registry.md) has the one-time setup.

```shell
npx shadcn@latest add https://www.box-kite.dev/r/credit-card-form.json
```

[Open in v0](https://v0.dev/chat/api/open?url=https%3A%2F%2Fwww.box-kite.dev%2Fr%2Fcredit-card-form.json)

[The item JSON](https://www.box-kite.dev/r/credit-card-form.json)

1 file · installs @box-kite/react, store-design

## Light and dark

The same component twice, each half in a theme of its own — nothing in it is written for either. [Open it full screen](https://www.box-kite.dev/blocks/react-credit-card-form/demo.md) to see it in yours.

## Source

The files the command writes, exactly as the preview above runs them. Every style is a prop, so there is no stylesheet to copy with them.

**box-kite/store/credit-card-form.tsx**

```jsx
'use client';
import Box from '@box-kite/react';
import Flex from '@box-kite/react/components/flex';
import Grid from '@box-kite/react/components/grid';
import { Span } from '@box-kite/react/components/semantics';
import Textbox from '@box-kite/react/components/textbox';
import { useState } from 'react';
import FormField from './form-field';

type CardField = 'number' | 'name' | 'expiry' | 'cvc';

const digits = (value: string) => value.replace(/\D/g, '');

/** Groups of four as the reader types, which is how the number is printed on the card in their hand. */
const formatCardNumber = (value: string) =>
  digits(value)
    .slice(0, 19)
    .replace(/(\d{4})(?=\d)/g, '$1 ');

/** `MM / YY`, with the slash written for them once the month is whole. */
const formatExpiry = (value: string) => {
  const raw = digits(value).slice(0, 4);

  return raw.length > 2 ? `${raw.slice(0, 2)} / ${raw.slice(2)}` : raw;
};

/** The Luhn checksum every card number carries: catches a mistyped or transposed digit before a server is asked. */
function passesLuhn(value: string) {
  const number = digits(value);
  if (number.length < 12) return false;

  let sum = 0;
  for (let index = 0; index < number.length; index++) {
    let digit = Number(number[number.length - 1 - index]);
    if (index % 2) digit = digit * 2 > 9 ? digit * 2 - 9 : digit * 2;
    sum += digit;
  }

  return sum % 10 === 0;
}

function problemWith(field: CardField, value: string, today = new Date()): string | undefined {
  if (!value.trim()) return 'Required.';
  if (field === 'number' && !passesLuhn(value)) return 'That card number does not look right — check the digits.';
  if (field === 'cvc' && !/^\d{3,4}$/.test(value)) return 'Three digits on the back, four on the front of some cards.';
  if (field === 'expiry') {
    const [month, year] = value.split(' / ').map(Number);
    if (!month || month > 12 || !year) return 'Write it as MM / YY.';
    if (new Date(2000 + year, month) <= today) return 'This card has expired.';
  }

  return undefined;
}

/**
 * A card payment fieldset: number, name, expiry and security code, formatted as they are typed and checked
 * when a field is left, with a card drawn above that fills in as they go. It is a `<fieldset>`, not a form,
 * so it drops into a checkout form beside the address; the fields submit as `cardNumber`, `cardName`,
 * `cardExpiry` and `cardCvc`, with the `autocomplete` tokens a browser fills a saved card from.
 *
 * This only shapes the input. Sending a raw card number to your own server puts you in scope for PCI DSS —
 * in production these fields are usually your payment provider's hosted inputs, laid out the same way.
 */
export default function CreditCardForm() {
  const [values, setValues] = useState<Record<CardField, string>>({ number: '', name: '', expiry: '', cvc: '' });
  const [touched, setTouched] = useState<Partial<Record<CardField, boolean>>>({});

  const error = (field: CardField) => (touched[field] ? problemWith(field, values[field]) : undefined);
  const set = (field: CardField, value: string) => setValues((current) => ({ ...current, [field]: value }));
  const leave = (field: CardField) => () => setTouched((current) => ({ ...current, [field]: true }));

  return (
    <Box tag="fieldset" b={0} p={0} m={0} minWidth={0}>
      <Box tag="legend" fontSize={16} fontWeight={500} mb={5}>
        Payment
      </Box>

      <Card number={values.number} name={values.name} expiry={values.expiry} />

      <Flex d="column" gap={4} mt={5}>
        <FormField label="Card number" error={error('number')}>
          {(control) => (
            <Textbox
              name="cardNumber"
              value={values.number}
              onChange={(event) => set('number', formatCardNumber(event.target.value))}
              placeholder="1234 5678 9012 3456"
              props={{ ...control, inputMode: 'numeric', autoComplete: 'cc-number', onBlur: leave('number') }}
            />
          )}
        </FormField>
        <FormField label="Name on card" error={error('name')}>
          {(control) => (
            <Textbox
              name="cardName"
              value={values.name}
              onChange={(event) => set('name', event.target.value)}
              props={{ ...control, autoComplete: 'cc-name', onBlur: leave('name') }}
            />
          )}
        </FormField>
        <Grid gridTemplateColumns={2} gap={4}>
          <FormField label="Expiry" error={error('expiry')}>
            {(control) => (
              <Textbox
                name="cardExpiry"
                value={values.expiry}
                onChange={(event) => set('expiry', formatExpiry(event.target.value))}
                placeholder="MM / YY"
                props={{ ...control, inputMode: 'numeric', autoComplete: 'cc-exp', onBlur: leave('expiry') }}
              />
            )}
          </FormField>
          <FormField label="Security code" error={error('cvc')}>
            {(control) => (
              <Textbox
                name="cardCvc"
                value={values.cvc}
                onChange={(event) => set('cvc', digits(event.target.value).slice(0, 4))}
                placeholder="123"
                props={{ ...control, inputMode: 'numeric', autoComplete: 'cc-csc', onBlur: leave('cvc') }}
              />
            )}
          </FormField>
        </Grid>
      </Flex>
    </Box>
  );
}

/** The card as it fills in. Decoration: every character on it is already in a labelled field below. */
function Card({ number, name, expiry }: { number: string; name: string; expiry: string }) {
  const shown = digits(number)
    .padEnd(16, '•')
    .replace(/(.{4})(?=.)/g, '$1 ');

  return (
    <Flex
      d="column"
      jc="space-between"
      aspectRatio={1.586}
      maxWidth={88}
      p={5}
      borderRadius={3}
      color="stone-100"
      bgColor="stone-900"
      theme={{ dark: { bgColor: 'stone-800', ring: 1, ringColor: 'white/10' } }}
      props={{ 'aria-hidden': true }}
    >
      <Flex jc="space-between" ai="start">
        <Box width={9} height={7} borderRadius={1} bgColor="stone-400" />
        <Span fontSize={12} fontWeight={500} letterSpacing={2} color="stone-400">
          DEBIT
        </Span>
      </Flex>
      <Span fontSize={20} letterSpacing={2} css={{ fontVariantNumeric: 'tabular-nums' }}>
        {shown}
      </Span>
      <Flex jc="space-between" fontSize={12} textTransform="uppercase" letterSpacing={1}>
        <Span overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap">
          {name || 'Your name'}
        </Span>
        <Span>{expiry || 'MM / YY'}</Span>
      </Flex>
    </Flex>
  );
}
```

It stands on `store-design`, which the command installs with it. The design the store blocks and pages share: the components restyled with Box.createComponents(), the catalogue, the drawn product art, and the header and footer. [The design is on the template page.](https://www.box-kite.dev/templates/react-checkout-page.md#design)

## Used in

- [React checkout page template](https://www.box-kite.dev/templates/react-checkout-page.md)

---

_Every page: https://www.box-kite.dev/llms.txt · every prop, measured: https://www.box-kite.dev/props.md_

