PaymentCardInput
A frame around your payment provider's card field, so it matches the rest of the form.
SourcePackage @kerf-js/veneer
Preview
tsx
import { PaymentCardInput } from '@kerf-js/veneer'import type { ProviderAppearance, ProviderField} from '@kerf-js/veneer'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 @kerf-js/veneerUsage
tsx
import { PaymentCardInput } from '@kerf-js/veneer'<PaymentCardInput label="Card details" mount={testProvider}/>States
Errors come from the provider's change event.
tsx
import { Grid, PaymentCardInput, Stack, Text} from '@kerf-js/veneer'import type { ProviderAppearance, ProviderField} from '@kerf-js/veneer'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 = ({ name, children,}: { name: string children?: unknown}) => ( <Stack gap="tight"> <Text role="caption" subtle > {name} </Text> {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 '@kerf-js/core'import { Button, Input, PaymentCardInput, Row, Stack, Text} from '@kerf-js/veneer'import type { ProviderAppearance, ProviderField} from '@kerf-js/veneer'export function Checkout() { const complete = state(false) const paid = state(false) const pay = (e: Event) => { e.preventDefault() paid.value = true } return ( <Stack as="form" gap="group" onSubmit={pay} > <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
- Veneer 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 veneer({ 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