Card Form
- Stable
- WCAG 2.2 evidence
- RTL
A form for entering a payment card. Built on React
Aria Form and TextField: every field has a visible label tied to its input, and the errors are inline, with an icon and
text, linked with aria-describedby and shown after a field loses focus or the form is submitted. A submit with errors
announces how many fields need attention and moves focus to the first one (WCAG 1.3.1, 3.3.1, 3.3.2, 4.1.2, 4.1.3). The Figma states are not props: Empty, Typing (focus border), Valid (a check in the number box once it is complete and passes
the checksum) and Error (a message under the field, for example "This card has expired") follow from what is typed.
Typing formats as you go (groups of four, MM/YY) without blocking paste or autofill (3.3.7, 3.3.8); the fields carry the
cc-number, cc-exp, cc-csc and cc-name autocomplete tokens (1.3.5); digits are monospaced and stay left-to-right in
right-to-left pages. At 320 px the expiry and CVC fields stack (1.4.10). The form never stores or logs card data: it only hands the values to onSubmit. Do not use it to keep card data in your own
system; use your payment provider's hosted fields where possible. Consumer duties: translated labels, the secure note
wording, and what happens after submit.
import { CardForm } from "@nexera-ui/react";- 11
- examples
- 18
- props
- 2
- live controls
- 1
- platform
- 15
- WCAG criteria
- 2
- blocks use it
Try every prop. Copy the code.
Change the props and the code updates. Check light and dark, LTR and RTL, and phone width.
import { CardForm } from "@nexera-ui/react";
<CardForm />Examples 10
The same examples as Storybook, rendered live. Open Code to copy one.
Typing
Figma State=Typing: a card number half typed, with the focus border on the field (focus is set when the story loads).
import { CardForm } from "@nexera-ui/react";
const today = new Date(2026, 9, 4);
<CardForm today={today} defaultValue={{ number: "44171234", name: "Ayesha Khan" }} />;
Valid
Figma State=Valid: a complete, valid card. The number box shows the network and a check.
import { CardForm } from "@nexera-ui/react";
const today = new Date(2026, 9, 4);
const filled: CardFormValues = {
number: "4417123456789113",
expiry: "09/29",
cvc: "123",
name: "Ayesha Khan",
};
<CardForm today={today} defaultValue={filled} />;
Expired
Figma State=Error: the expiry is in the past, so after the form is submitted the field shows its message ("This card has expired") with an icon and the danger border, and focus moves to it.
import { CardForm } from "@nexera-ui/react";
const today = new Date(2026, 9, 4);
<CardForm today={today} defaultValue={{ ...filled, expiry: "09/24" }} />;
All errors
Submitting an empty form shows every error, announces how many fields need attention and focuses the first one.
import { CardForm } from "@nexera-ui/react";
const today = new Date(2026, 9, 4);
<CardForm today={today} />;
Disabled
isDisabled disables all fields, for example while the payment is being processed.
import { CardForm } from "@nexera-ui/react";
const today = new Date(2026, 9, 4);
const filled: CardFormValues = {
number: "4417123456789113",
expiry: "09/29",
cvc: "123",
name: "Ayesha Khan",
};
<CardForm today={today} defaultValue={filled} isDisabled />;
Custom network tile
renderNetwork puts your own logo in the 38 x 24 px tile; the default shows the network's name.
import { CardForm } from "@nexera-ui/react";
const today = new Date(2026, 9, 4);
<CardForm
today={today}
defaultValue={{ number: "5555555555554444" }}
renderNetwork={(network) => (
<span
aria-label={network}
className="size-full bg-brand-subtle text-center text-body-micro leading-6 text-brand-text"
role="img"
>
{network.slice(0, 2).toUpperCase()}
</span>
)}
/>;
Submitting
Submit a valid card: onSubmit receives the normalised values and the network. Enter in any field submits.
import { useState } from "react";
import { Button, CardForm, type CardFormProps } from "@nexera-ui/react";
const today = new Date(2026, 9, 4);
function SubmitDemo(args: CardFormProps) {
const [saved, setSaved] = useState<string | null>(null);
return (
<div className="flex flex-col gap-4">
<CardForm
{...args}
onSubmit={(card) => {
// Demo only: a real app hands the values to its payment provider and never stores or logs them.
setSaved(`${card.network ?? "card"} ending ${card.number.slice(-4)} for ${card.name}`);
}}
>
<Button type="submit">Save card</Button>
</CardForm>
<p className="m-0 text-body-small text-secondary" role="status">
{saved === null ? "Nothing saved yet." : `Saved: ${saved}`}
</p>
</div>
);
}
export function Submitting() {
return <SubmitDemo today={today} />;
}
Translated
Every text is a prop: translate labels (the messages, placeholders and the screen reader summary too).
import { CardForm } from "@nexera-ui/react";
const today = new Date(2026, 9, 4);
<CardForm
today={today}
defaultValue={{ number: "44171234" }}
secureNote="Kartendaten werden von unserem Zahlungsanbieter verschluesselt gespeichert."
labels={{
cardNumber: "Kartennummer",
expiry: "Gueltig bis",
cvc: "Pruefnummer",
nameOnCard: "Name auf der Karte",
numberPlaceholder: "1234 1234 1234 1234",
expiryPlaceholder: "MM / JJ",
cvcPlaceholder: "3 Ziffern",
namePlaceholder: "Wie auf der Karte gedruckt",
numberInvalid: "Diese Kartennummer ist ungueltig. Pruefe die Ziffern.",
expiryRequired: "Gib das Ablaufdatum ein.",
cvcRequired: "Gib die Pruefnummer ein.",
nameRequired: "Gib den Namen auf der Karte ein.",
errorSummary: (count) => `${String(count)} Felder muessen geprueft werden.`,
}}
/>;
Narrow container
At 18 rem the expiry and CVC fields stack; from 20 rem they sit side by side.
import { CardForm } from "@nexera-ui/react";
const today = new Date(2026, 9, 4);
<div className="max-w-72">
<CardForm today={today} />
</div>;
Right to left
Right-to-left: labels and the name field follow the page direction; card number, expiry and CVC stay left-to-right.
import { CardForm } from "@nexera-ui/react";
const today = new Date(2026, 9, 4);
<CardForm
today={today}
defaultValue={{ ...filled, name: "عائشة خان" }}
secureNote="تُشفَّر بيانات البطاقة ويحفظها مزود الدفع لدينا."
labels={{
cardNumber: "رقم البطاقة",
expiry: "تاريخ الانتهاء",
cvc: "رمز الأمان",
nameOnCard: "الاسم على البطاقة",
cvcHelp: "الأرقام الثلاثة الموجودة على ظهر البطاقة.",
}}
/>;
Props 18
Press "Try it" on a card to load that prop into the playground.
18 props shown
valueNexeraCardFormValuesThe values (controlled). Use with onChange. number is digits only, expiry is MM/YY while typed, cvc digits.
defaultValueNexeraPartial<CardFormValues>The values on first render (uncontrolled). Leave out the fields that start empty.
onChangeNexera(values: CardFormValues) => voidCalled with the new values whenever a field changes (the number as digits, without spaces).
onSubmitNexera(values: CardFormValues & { network: CardNetwork; }) => voidCalled when the form is submitted and every field is valid, with the normalised values. When something is wrong the form shows the errors, announces them, and moves focus to the first invalid field instead. Hand the values straight to your payment provider's client; never log or store them.
isDisabledNexerabooleanDisables all fields.
labelsNexeraPartial<CardFormLabels>Replaces any of the visible texts, messages and placeholders (translate them). Missing keys keep the English default.
secureNoteNexeraReactNodeThe note under the fields. Say who stores the details. Pass null to hide it.
renderNetworkNexera(network: CardNetwork) => ReactNodeContent of the network tile in the card number box, for example your logo of the network. It receives the detected network; the tile shows once the number's first digits are recognised. By default the tile holds the network's name as text.
todayNexeraDateThe date used to decide whether the expiry is in the past. Defaults to now; set it in tests and stories so that results do not depend on the day.
childrenNexeraReactNodeContent after the note, for example the submit button: <Button type="submit">Save card</Button>.
classNameNexerastringExtra classes for the form, merged last so they win over the defaults.
autoCapitalizeReact Aria"off" | "none" | "on" | "sentences" | "words" | "characters"Controls whether inputted text is automatically capitalized and, if so, in what manner. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/autocapitalize).
roleReact Aria"search" | "presentation"An ARIA role override to apply to the form element.
aria-labelReact AriastringDefines a string value that labels the current element.
aria-labelledbyReact AriastringIdentifies the element (or elements) that labels the current element.
aria-describedbyReact AriastringIdentifies the element (or elements) that describes the object.
styleReact AriaCSSPropertiesThe inline [style](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/style) for the element.
idReact AriastringThe element's unique identifier. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/id).
* Required. React Aria props shown are the ones most apps use; the component accepts the rest of its React Aria props too.
Accessible by default.
Built on React Aria, and covered by the WCAG 2.2 evidence generated on every build.
WCAG 2.2 evidence
13 direct · 2 supporting- 1.1.1Non-text ContentLevel A · tested directly
- 1.3.1Info and RelationshipsLevel A · tested directly
- 1.3.2Meaningful SequenceLevel A · supporting test
- 1.3.5Identify Input PurposeLevel AA · tested directly
- 1.4.1Use of ColorLevel A · tested directly
- 1.4.10ReflowLevel AA · supporting test
- 2.1.1KeyboardLevel A · tested directly
- 3.1.2Language of PartsLevel AA · tested directly
- 3.3.1Error IdentificationLevel A · tested directly
- 3.3.2Labels or InstructionsLevel A · tested directly
- 3.3.3Error SuggestionLevel AA · tested directly
- 3.3.7Redundant EntryLevel A · tested directly
- 3.3.8Accessible Authentication (Minimum)Level AA · tested directly
- 4.1.2Name, Role, ValueLevel A · tested directly
- 4.1.3Status MessagesLevel AA · tested directly
Fits any width.
Nexera components respond to the space they are given. Drag the corner of the frame, or pick a width.
Styling hooks
Pass className to add Tailwind classes (merged last). State is exposed as data attributes, so you can style it with variants like data-pressed:.
<CardForm className="shadow-sm" />Used in blocks
Related components
- AccordionItemOne collapsible section of an FAQ or settings page.
- AreaChartA stacked area chart: layers piled up over time with the title, range switch, actions and legend of the card.
- AvailabilityRowOne person's availability across a window of the day: an avatar with initials and the name, and a timeline with the busy periods drawn as bars.
- AvatarA person or organisation shown as a photo, initials or an icon.
- AvatarGroupA row of overlapping avatars with a "+N" chip for the rest.
- AvatarLabelAn avatar with a name and an optional subtitle beside it, for a person in a list, a header or a mention.
- BadgeShort status or category label: `Tone` x `Variant` x `Size`, with an optional dot, leading icon and remove button.
- BadgeGroupA pill that pairs a small badge with a short message, for an announcement or a status line.