> ## Documentation Index
> Fetch the complete documentation index at: https://docs.henrylabs.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Card Element

> Securely collect payment card details in an isolated iframe and tokenize them for server-side processing.

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](/v1/sdk/server/guides/checkout#headless-checkout) flow.

<Warning>
  Because the card is captured and tokenized inside the iframe, raw card data
  never reaches your servers - this keeps you out of PCI DSS scope. A
  server-side `POST /card/tokenize` REST endpoint also exists (it is not
  exposed through the SDK), but passing the raw PAN, CVV, and expiry through
  your own backend brings your systems into PCI scope (typically SAQ D).
  Prefer this element unless you already run a PCI-compliant environment.
</Warning>

***

## Usage

```js theme={null}
import Henry from '@henrylabs/js';

const cardEl = Henry.createCardElement('#card-container');
```

Add a container in your HTML:

```html theme={null}
<div id="card-container"></div>
```

To submit, call `validate()` then `submit()`:

```js theme={null}
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.

```js theme={null}
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

| Field              | Type               | Description                                     |
| ------------------ | ------------------ | ----------------------------------------------- |
| `cardNumber`       | `CardElementStyle` | Styles for the card number field                |
| `cardExpiration`   | `CardElementStyle` | Styles for the expiration date field            |
| `cardVerification` | `CardElementStyle` | Styles for the CVV/CVC field                    |
| `container`        | `CSSProperties`    | Styles applied to the outer container element   |
| `inputWrappers`    | `CSSProperties`    | Styles applied to each field wrapper            |
| `section1`         | `CSSProperties`    | Styles applied to the first row (card number)   |
| `section2`         | `CSSProperties`    | Styles applied to the second row (expiry + CVV) |
| `fonts`            | `string[]`         | Font URLs to load inside the iframe             |

#### `CardElementStyle` reference

Each field style (`cardNumber`, `cardExpiration`, `cardVerification`) accepts:

| Field            | Type                      | Description                               |
| ---------------- | ------------------------- | ----------------------------------------- |
| `base`           | `CardElementStyleVariant` | Default state styles                      |
| `complete`       | `CardElementStyleVariant` | Styles when the field is filled and valid |
| `invalid`        | `CardElementStyleVariant` | Styles when the field contains an error   |
| `empty`          | `CardElementStyleVariant` | Styles when the field is empty            |
| `container`      | `CardElementStyleVariant` | Styles for the field's inner container    |
| `placeholder`    | `string`                  | Custom placeholder text                   |
| `label`          | `string`                  | Custom label text                         |
| `labelContainer` | `CSSProperties`           | Styles for the label element              |
| `fonts`          | `string[]`                | Field-specific font URLs                  |

`CardElementStyleVariant` supports these CSS properties:

```ts theme={null}
{
  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.

```js theme={null}
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.

```js theme={null}
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.

```js theme={null}
cardEl.destroy();
```

***

## Full example

```html theme={null}
<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

```tsx theme={null}
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>
	);
}
```

***

## Related

<CardGroup cols={2}>
  <Card title="Checkout Widget" icon="cart-shopping" href="/v1/sdk/client/widgets/checkout">
    Embed a complete buy-now checkout button and modal
  </Card>

  <Card title="Server SDK - Checkout" icon="server" href="/v1/sdk/server/guides/checkout#headless-checkout">
    Build headless checkout flows server-side
  </Card>
</CardGroup>
