Payments
24. AmountInput #
The hardest input in fintech. If this is wrong, nothing else matters.
<AmountInput
currency="USD"
value={amount}
onChange={setAmount}
max={availableBalance}
onMaxClick={() => setAmount(availableBalance)}
/>
The rules#
type="text"+inputMode="decimal". Nevertype="number". Its scroll wheel increments the value (and will silently change someone's payment), it parses decimals differently by locale, and its spinners can't be styled.- 16px minimum font size or iOS Safari zooms the viewport on focus and never zooms back.
- Parse permissively, format on blur. Accept
1,234.56,1234,56,1 234.56,$1234.56,1.2k. Reject nothing a person would type. Format canonically when they leave the field. - Never reformat mid-typing. Inserting a thousands separator mid-typing moves the caret. It's the most infuriating input bug in fintech. Format on blur only.
- Caret preservation if you must transform during input — measure the caret offset from the right, not the left.
- Never block paste.
- Locale-aware decimal separator. A German user types
1234,56. Accept it. - Currency-aware precision. JPY rejects decimals entirely; KWD accepts three.
- Max is soft by default. Show "Exceeds available balance of $4,120.00" as an inline error, but let them type it. An overdraft may be legitimate, and blocking input to prevent an invalid state blocks valid ones too. The check — and the Max button — apply only when
maxis in the field's own currency; a balance in another currency is shown but never compared. - Big.
figure-lg(28/32) or larger. It's the most important number on the screen and should look it. - The symbol is part of the field, at
--text-tertiary, not a floating adornment that misaligns as the value grows.
Accessibility#
aria-label="Amount in US dollars" · parsed value announced on blur ("one thousand two hundred thirty-four dollars and fifty-six cents") via aria-describedby · errors specific and actionable · aria-invalid on error.
Motion#
None on the value. Error message: 140ms opacity + 4px translate. Shake on invalid submit is forbidden. It signals stress to a user who's already anxious about money.
25. CurrencySelect#
Searchable by code, name, and symbol. Recents pinned at the top. Flag optional, code always. Virtualized (180+ currencies) — fixed-height rows windowed by hooks/use-option-keys, with the combobox + listbox pattern. Keyboard typeahead: typing eu selects EUR.
26. PayeeSelect#
Fuzzy search across name, account, and reference. Shows the last-paid amount and date inline, the most useful context when choosing a recipient. New-payee flow inline, not a separate route.
27. PaymentForm#
amount → payee → method → date → memo. Amount comes first because it's what the user came to do, and it gives context to everything after.
- Progressive disclosure: memo, reference, and attachments behind "Add details".
- Validation on blur, never per-keystroke.
- Submit is bottom-anchored on phone, above the safe area and above the keyboard (
visualViewport), always visible. - The submit button states the amount:
Send $12,400.00. - Fee and arrival estimate update live below the amount with a value crossfade, never a count-up.
28. PaymentMethodList#
Radio cards with EntityMark, masked number, and any per-method fee/timing difference stated inline. Selected state: 2px --accent border, an --accent-subtle background, and a check glyph, because border color alone fails forced-colors mode.
29. CheckoutSummary#
Line items → subtotal (single rule) → fees → tax → total (double rule). Uses the same Rule grammar as the statements. Fees are itemized and named, never a lump "Fees". Multi-currency lines show conversion with the rate and its timestamp.
30. ConfirmPaymentDialog#
Amount, destination, timing. In that order. Every time.
Send $12,400.00 to Northwind Ltd?
To account ••••4291 · Arrives 8 Sep
This can't be undone once submitted.
[Cancel] [Hold to send →]
AlertDialog. Focus lands on Cancel, never on the destructive action. Escape cancels.
For irreversible movements (wires, ACH past cutoff) the confirm is hold-to-confirm: 1500ms linear fill, 200ms release, slow where the user is deciding. The full spec, with keyboard and typed-confirmation fallbacks, is in the motion catalog. Hold can never be the only path; gating money behind a motor skill is an accessibility failure. The typed word is typedConfirmation, default SEND.
31. PaymentStatusTimeline#
Submitted → Sent → In transit → Settled, each with a real timestamp and timezone. Failed states show the reason and the remedy. Pending is a state, not a load: a clock icon, never a spinner.
32. PaymentSuccess#
The sanctioned delight moment. Circle scale → checkmark stroke draw → details fade up, staggered. Total ≤ 700ms. No confetti, no bounce, no sound. Spec.
Shows the amount, the destination and the arrival estimate, which is what the user needs before closing the screen.
33. ReceiptSheet#
Printable and shareable. Entity, amount, payee, method, reference, timestamp with timezone. Print styles are first-class because people keep this document.
34. PayoutSchedule#
Upcoming payouts on a calendar or list, with cutoff times stated in the user's timezone. Business days respected, holidays flagged.
35. CardInput#
PAN / expiry / CVC. autocomplete="cc-number" | "cc-exp" | "cc-csc", Luhn validation, network detection from the IIN with the mark appearing inline (140ms crossfade, blur-masked). Auto-advances between fields, and backspace returns to the previous field, which most implementations forget.
36. BankAccountInput#
Routing + account number. ABA checksum validation, then the institution name appears once valid, so the user knows right away they typed the right bank. IBAN mod-97 validation for international, with country-aware length and formatting.
37. RecurringSchedule#
Frequency, start, end condition. Always previews the next three occurrences with resolved dates. "Monthly on the 31st" is ambiguous in February; the preview shows how it resolves before it surprises anyone. The preview honors the end condition: the start date is the first payment, "after N payments" and "on a date" cut the list short ("All 2 payments") and state the last payment and the count, and an end date before the start says so instead of listing dates. Uncontrolled via defaultFrequency / defaultStart / defaultValue, or controlled via value + onChange.