PaymentCardInput
A frame around your payment provider's card field, so it matches the rest of the form.
SourcePackage @weft-js/warp
Preview
tsx
import { PaymentCardInput } from '@weft-js/warp'import type { ProviderAppearance, ProviderField } from '@weft-js/warp'export function Basic() { return ( <PaymentCardInput label="Card details" mount={testProvider} /> )}function testProvider(el: HTMLElement, look: ProviderAppearance): ProviderField { const input = el.ownerDocument.createElement('input') input.inputMode = 'numeric' input.setAttribute('aria-label', 'Test card number') Object.assign(input.style, { all: 'unset', width: '100%', font: `${look.fontSize} ${look.fontFamily}`, color: look.color }) input.onfocus = () => (input.placeholder = '4242 4242 4242 4242') input.onblur = () => (input.placeholder = '') el.append(input) return { on(event, send) { if (event !== 'change') return input.addEventListener(event, () => send({})) input.addEventListener('input', () => { const digits = input.value.replace(/\s/g, '') send({ complete: digits.length === 16, error: /\D/.test(digits) ? 'A card number only has digits.' : undefined }) }) }, destroy: () => input.remove(), }}Installation
npm install @weft-js/warpUsage
tsx
import { PaymentCardInput } from '@weft-js/warp'<PaymentCardInput label="Card details" mount={testProvider} />States
Errors come from the provider's change event.
tsx
import { Grid, PaymentCardInput, Stack, Text } from '@weft-js/warp'import type { ProviderAppearance, ProviderField } from '@weft-js/warp'export function States() { return ( <Grid cols={3} gap="group"> <Cell name="Empty"><PaymentCardInput label="Card details" mount={testProvider} /></Cell> <Cell name="Hint"><PaymentCardInput label="Card details" mount={testProvider} hint="Charged on the 1st of each month." /></Cell> <Cell name="Invalid"><PaymentCardInput label="Card details" mount={failedProvider} /></Cell> </Grid> )}const Cell = (p: { name: string; children?: unknown }) => ( <Stack gap="tight"> <Text role="caption" subtle>{p.name}</Text> {p.children} </Stack>)function testProvider(el: HTMLElement, look: ProviderAppearance): ProviderField { const input = el.ownerDocument.createElement('input') input.inputMode = 'numeric' input.setAttribute('aria-label', 'Test card number') Object.assign(input.style, { all: 'unset', width: '100%', font: `${look.fontSize} ${look.fontFamily}`, color: look.color }) input.onfocus = () => (input.placeholder = '4242 4242 4242 4242') input.onblur = () => (input.placeholder = '') el.append(input) return { on(event, send) { if (event !== 'change') return input.addEventListener(event, () => send({})) input.addEventListener('input', () => { const digits = input.value.replace(/\s/g, '') send({ complete: digits.length === 16, error: /\D/.test(digits) ? 'A card number only has digits.' : undefined }) }) }, destroy: () => input.remove(), }}const failedProvider = (el: HTMLElement, look: ProviderAppearance) => { const field = testProvider(el, look) return { ...field, on: (event: Parameters<ProviderField['on']>[0], send: Parameters<ProviderField['on']>[1]) => (event === 'change' ? queueMicrotask(() => send({ error: 'This card was declined.' })) : field.on(event, send)) }}A checkout's card step
Listen to the provider's change event in your mount: Pay waits until it says the card is complete.
tsx
import { state } from '@weft-js/core'import { Button, Input, PaymentCardInput, Row, Stack, Text } from '@weft-js/warp'import type { ProviderAppearance, ProviderField } from '@weft-js/warp'export function Checkout() { const complete = state(false) const paid = state(false) return ( <Stack as="form" gap="group" onSubmit={(e: Event) => { e.preventDefault(); paid.value = true }}> <Text role="heading">Pay for Team</Text> <Input label="Name on card" autocomplete="cc-name" defaultValue="Mara Lindqvist" /> <PaymentCardInput label="Card details" hint="Try 4242 4242 4242 4242." mount={watchedProvider(c => (complete.value = c))} /> <Row justify="between"> <Text subtle role="caption">{paid.value ? 'Paid. A receipt is on its way.' : '$160 today, then monthly.'}</Text> <Button type="submit" aria-disabled={complete.value ? undefined : 'true'}>Pay $160</Button> </Row> </Stack> )}function testProvider(el: HTMLElement, look: ProviderAppearance): ProviderField { const input = el.ownerDocument.createElement('input') input.inputMode = 'numeric' input.setAttribute('aria-label', 'Test card number') Object.assign(input.style, { all: 'unset', width: '100%', font: `${look.fontSize} ${look.fontFamily}`, color: look.color }) input.onfocus = () => (input.placeholder = '4242 4242 4242 4242') input.onblur = () => (input.placeholder = '') el.append(input) return { on(event, send) { if (event !== 'change') return input.addEventListener(event, () => send({})) input.addEventListener('input', () => { const digits = input.value.replace(/\s/g, '') send({ complete: digits.length === 16, error: /\D/.test(digits) ? 'A card number only has digits.' : undefined }) }) }, destroy: () => input.remove(), }}function watchedProvider(done: (complete: boolean) => void) { return (el: HTMLElement, look: ProviderAppearance): ProviderField => { const field = testProvider(el, look) return { ...field, on: (event, send) => field.on(event, event === 'change' ? e => { done(!!e.complete); send(e) } : send) } }}When to use
Use it for
- Taking a card with Stripe, Adyen or similar
Reach for something else
- CardNumberText, when you're showing a card already saved
Accessibility
- warp never draws an input for a real card number. The number stays inside the provider's frame.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | The visible name, and what screen readers announce. |
hint | unknown | — | A short line of help under the field. |
error | unknown | — | Replaces the hint and marks the field invalid. |
icon | IconRef | — | Leading icon: it sweeps with the border. |
prefix | unknown | — | Text or an element inside the field before the value, such as $. |
suffix | unknown | — | Text or an element inside the field after the value, such as kg. |
variant | "floating" | "classic" | — | floating (default, or the app's warp({ fields })) · classic: the label above. |
appearance | "line" | — | line: only a bottom line under the value, no box or fill. The boxed field otherwise. |
size | "sm" | "md" | "lg" | 'md' | Control size. md by default. |
clearable | boolean | false | A clear button (shown once there is a value). |
counter | number | — | Maximum length, shown as a counter. |
id | string | — | The element id. One is made for you if you leave it out. |
disabled | boolean | false | Can't be focused or changed. |
readonly | boolean | false | Shows the value and lets it be selected and copied, but not changed. |
mount | (el: HTMLElement, appearance: ProviderAppearance) => ProviderField | — | Mounts the provider's field in the element it's given, and returns its on and destroy. |
| Event | Type | Default | Description |
|---|---|---|---|
onInput | ((e: InputEvent & { currentTarget: HTMLInputElement & HTMLTextAreaElemen… | — | Called on each keystroke. |
Parts
Every element carries a stable name, so you can style it from your own CSS: [data-scope="payment-card-input"] [data-part="provider"]
provider
RelatedCard number textInput