Skip to main content
The Card Element renders a secure card input form inside an iframe. Card data never touches your page - it’s collected entirely within the iframe, then tokenized. You receive a short-lived cardToken to pass to your server for payment processing. This element is the standard way to collect card details in a Headless Checkout flow.

Usage

import Henry from '@henrylabs/js';

const cardEl = Henry.createCardElement('#card-container');
Add a container in your HTML:
<div id="card-container"></div>
To submit, call validate() then submit():
const isValid = await cardEl.validate();

if (isValid) {
	const { cardToken } = await cardEl.submit();
	// send cardToken to the server SDK
}

Options

styles CardStyles

Optional. Customize the appearance of the card input fields.
const cardEl = Henry.createCardElement('#card-container', {
	styles: {
		cardNumber: {
			base: {
				color: '#111',
				fontSize: '16px',
				fontFamily: 'Inter, sans-serif',
			},
			'::placeholder': {
				color: '#aaa',
			},
			invalid: {
				color: '#e53e3e',
			},
		},
		cardExpiration: {
			base: { color: '#111' },
		},
		cardVerification: {
			base: { color: '#111' },
		},
	},
});

CardStyles reference

FieldTypeDescription
cardNumberCardElementStyleStyles for the card number field
cardExpirationCardElementStyleStyles for the expiration date field
cardVerificationCardElementStyleStyles for the CVV/CVC field
containerCSSPropertiesStyles applied to the outer container element
inputWrappersCSSPropertiesStyles applied to each field wrapper
section1CSSPropertiesStyles applied to the first row (card number)
section2CSSPropertiesStyles applied to the second row (expiry + CVV)
fontsstring[]Font URLs to load inside the iframe

CardElementStyle reference

Each field style (cardNumber, cardExpiration, cardVerification) accepts:
FieldTypeDescription
baseCardElementStyleVariantDefault state styles
completeCardElementStyleVariantStyles when the field is filled and valid
invalidCardElementStyleVariantStyles when the field contains an error
emptyCardElementStyleVariantStyles when the field is empty
containerCardElementStyleVariantStyles for the field’s inner container
placeholderstringCustom placeholder text
labelstringCustom label text
labelContainerCSSPropertiesStyles for the label element
fontsstring[]Field-specific font URLs
CardElementStyleVariant supports these CSS properties:
{
  backgroundColor?: string
  color?: string
  fontFamily?: string
  fontSize?: string
  fontStyle?: string
  fontWeight?: string | number
  letterSpacing?: string
  lineHeight?: string | number
  padding?: string
  textAlign?: string
  textDecoration?: string
  textShadow?: string
  textTransform?: string

  // Pseudo-class overrides
  ":hover"?: { ... }
  ":focus"?: { ... }
  ":read-only"?: { ... }
  "::placeholder"?: { ... }
  "::selection"?: { ... }
  ":disabled"?: { ... }
}

Methods

validate()Promise<boolean>

Returns true if all card fields contain valid input. Use this before submitting to surface validation errors to the buyer.
const isValid = await cardEl.validate();
if (!isValid) {
	// show your own error message
}

submit()Promise<{ cardToken: string }>

Tokenizes the card and returns a cardToken. Pass this token to your server to complete the payment - never send raw card data from your frontend.
const { cardToken } = await cardEl.submit();

await fetch('/api/pay', {
	method: 'POST',
	body: JSON.stringify({ cardToken }),
	headers: { 'Content-Type': 'application/json' },
});

destroy()

Removes the iframe and cleans up all event listeners. Call this when navigating away or unmounting a component.
cardEl.destroy();

Full example

<form id="payment-form">
	<div id="card-container"></div>
	<button type="submit">Pay now</button>
</form>

<script type="module">
	import Henry from '@henrylabs/js';

	const cardEl = Henry.createCardElement('#card-container', {
		styles: {
			cardNumber: {
				base: { color: '#111', fontSize: '16px' },
				'::placeholder': { color: '#aaa' },
				invalid: { color: '#e53e3e' },
			},
			fonts: ['https://fonts.googleapis.com/css2?family=Inter&display=swap'],
		},
	});

	document.getElementById('payment-form').addEventListener('submit', async (e) => {
		e.preventDefault();

		const isValid = await cardEl.validate();
		if (!isValid) return;

		const { cardToken } = await cardEl.submit();

		await fetch('/api/pay', {
			method: 'POST',
			body: JSON.stringify({ cardToken }),
			headers: { 'Content-Type': 'application/json' },
		});
	});
</script>

React

import { useEffect, useRef } from 'react';
import Henry from '@henrylabs/js';

function CardForm() {
	const cardElRef = useRef<ReturnType<typeof Henry.createCardElement> | null>(null);
	const initialized = useRef(false);

	useEffect(() => {
		if (initialized.current) return;
		initialized.current = true;

		cardElRef.current = Henry.createCardElement('#card-container', {
			styles: {
				cardNumber: {
					base: { color: '#111', fontSize: '16px' },
				},
			},
		});

		return () => cardElRef.current?.destroy();
	}, []);

	async function handleSubmit(e: React.FormEvent) {
		e.preventDefault();
		if (!cardElRef.current) return;

		const isValid = await cardElRef.current.validate();
		if (!isValid) return;

		const { cardToken } = await cardElRef.current.submit();
		// send cardToken to the server SDK
	}

	return (
		<form onSubmit={handleSubmit}>
			<div id='card-container' />
			<button type='submit'>Pay now</button>
		</form>
	);
}

Checkout Widget

Embed a complete buy-now checkout button and modal

Server SDK - Checkout

Build headless checkout flows server-side