Skip to content
weftGitHub

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/warp

Usage

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

Accessibility

  • warp never draws an input for a real card number. The number stays inside the provider's frame.

API Reference

PropTypeDefaultDescription
labelstring—The visible name, and what screen readers announce.
hintunknown—A short line of help under the field.
errorunknown—Replaces the hint and marks the field invalid.
iconIconRef—Leading icon: it sweeps with the border.
prefixunknown—Text or an element inside the field before the value, such as $.
suffixunknown—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.
clearablebooleanfalseA clear button (shown once there is a value).
counternumber—Maximum length, shown as a counter.
idstring—The element id. One is made for you if you leave it out.
disabledbooleanfalseCan't be focused or changed.
readonlybooleanfalseShows 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.
EventTypeDefaultDescription
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