Element Events
Elements emit events that let your application respond to user interactions and state changes. All event data is accessed via event.detail.
Quick reference
| Event | Fired when | Payload extras |
|---|---|---|
ready | Element iframe is loaded and interactive | — |
change | Input value changes | isValid, isEmpty, error |
focus | Element receives focus | — |
blur | Element loses focus | — |
copy | A copyButton is clicked | success, error? |
error | An error occurs | code, message, status?, validationErrors? |
networkChanged | Selected network changes on a co-badged card (cardNumber and card only) | network, previousNetwork, availableNetworks, selectionMode |
Subscribing and unsubscribing
element.on() returns an unsubscribe function. Call it to remove the listener.
- Web Elements
- React Elements
// Subscribe
const unsubscribe = cardNumberEl.on('change', (event) => {
const { isValid, isEmpty, cardBrand } = event.detail;
console.log({ isValid, isEmpty, cardBrand });
});
// Unsubscribe when no longer needed
unsubscribe();
In React, use the onChange, onReady, onFocus, onBlur, and onError props directly on element components. The event object passed to each handler is a CustomEvent — access all data via event.detail.
import { CardNumberElement } from '@basis-theory/react-elements';
function PaymentForm() {
return (
<CardNumberElement
id="card-number"
onReady={(event) => console.log('ready', event.detail)}
onChange={(event) => {
const { isValid, isEmpty, cardBrand } = event.detail;
console.log({ isValid, isEmpty, cardBrand });
}}
onFocus={(event) => console.log('focused')}
onBlur={(event) => console.log('blurred')}
onError={(event) => console.error(event.detail.code, event.detail.message)}
/>
);
}
Event payloads
All events include the following base fields in event.detail:
| Field | Type | Description |
|---|---|---|
elementType | string | The type of element that fired the event ('cardNumber', 'cvv', 'expiry', 'text', 'copyButton') |
elementId | string | Unique element ID (useful when handling multiple elements) |
timestamp | number | Unix timestamp (milliseconds) when the event fired |
ready
Fired when the element iframe has loaded and the user can interact with it.
// event.detail shape
{
elementType: string;
elementId: string;
timestamp: number;
}
change
Fired whenever the input value changes.
// event.detail shape (all elements)
{
isValid: boolean; // true when the input passes all validation rules
isEmpty: boolean; // true when no characters have been entered
error: ValidationError | null; // null when valid
elementType: string;
elementId: string;
timestamp: number;
}
cardNumber element adds the following fields:
{
cardBrand?: string; // detected brand (e.g. 'Visa', 'Mastercard')
last4?: string | null; // last 4 digits (PCI-safe, for display)
bin?: string | null; // BIN (first 6–8 digits)
cvvLengths?: number[] | null; // expected CVV lengths for the detected brand
potentialBrands?: string[]; // all brands that could match the current input
matchStrength?: number; // confidence score (0–1)
networks?: string[]; // networks a co-badged card supports (requires BIN enrichment)
selectedNetwork?: string; // currently selected network on a co-badged card
binDetails?: BinDetails; // issuer metadata from BIN enrichment
}
The card element emits the same cardBrand, last4, bin, networks, selectedNetwork and binDetails fields, plus an errors array covering all three sub-fields.
BinDetails
Returned on the change event when BIN enrichment is on. Every field is optional — enrichment coverage varies by BIN, and a lookup that returns nothing usable omits binDetails entirely.
{
brand?: string; // card brand from enrichment, kebab-case (e.g. 'visa', 'cartes-bancaires')
type?: 'debit' | 'credit' | 'prepaid'; // funding type; omitted when not reported
country?: string; // issuing country, ISO 3166-1 alpha-2
bank?: string; // issuing bank name
segment?: string; // 'Consumer', 'Commercial', 'Business' or 'Government'
}
Two things to design around. binDetails arrives asynchronously — the lookup is debounced and then makes a network call, so it is absent for a short window after the sixth digit and stays absent if the lookup finds nothing. And it is scoped to the first six digits: unlike networks, it is not narrowed as more digits are entered.
binDetails.brand comes from the issuer's BIN record, while the top-level cardBrand comes from local pattern matching and is available immediately. They normally agree; prefer cardBrand for responsive UI and binDetails.brand when you need the issuer's own answer.
networks and selectedNetwork are populated only when the card participates in more than one network.
focus
Fired when the element receives keyboard focus.
// event.detail shape
{
elementType: string;
elementId: string;
timestamp: number;
}
blur
Fired when the element loses focus.
// event.detail shape
{
elementType: string;
elementId: string;
timestamp: number;
}
copy
Fired when a copyButton element is clicked. Only emitted by the copyButton element type. The payload reports whether the copy succeeded; it never contains the copied value.
// event.detail shape
{
success: boolean; // true if the value was copied to the clipboard
error?: string; // failure reason (no sensitive data) when success is false
elementType: string; // 'copyButton'
elementId: string;
timestamp: number;
}
In React, subscribe with the onCopy prop on CopyButtonElement:
<CopyButtonElement
allowClipboard
onCopy={(event) => {
if (event.detail.success) {
showToast('Copied to clipboard');
} else {
showToast(`Copy failed: ${event.detail.error}`);
}
}}
/>
error
Fired when an error occurs during element operations (mount failure, API error, etc.).
// event.detail shape
{
code: ErrorCode; // machine-readable code
message: string; // human-readable description
status?: number; // HTTP status (for API errors)
validationErrors?: Record<string, string[]>; // field-level errors from API
elementType: string;
elementId: string;
timestamp: number;
}
networkChanged
Fired when the selected payment network changes on a co-badged card — through automatic selection, the built-in selector, or a call to setNetwork(). Only cardNumber and card elements emit this event, and only when binLookup or coBadge is enabled.
// event.detail shape
{
network: string; // the newly selected network
previousNetwork?: string; // the network selected before this change
availableNetworks: string[]; // all networks the card supports
selectionMode: 'auto' | 'user'; // how the selection was made
elementType: string;
elementId: string;
timestamp: number;
}
- Web Elements
- React Elements
cardNumberEl.on('networkChanged', (event) => {
const { network, availableNetworks } = event.detail;
console.log('Network is now', network, 'of', availableNetworks);
});
import { CardNumberElement } from '@basis-theory/react-elements';
<CardNumberElement
id="card-number"
coBadge={{ preferredNetworks: ['cartes-bancaires', 'visa'] }}
onNetworkChanged={(event) => {
const { network, availableNetworks } = event.detail;
console.log('Network is now', network, 'of', availableNetworks);
}}
/>
See Co-Badge Support for the full co-badge workflow, including building your own network picker with setNetwork().
ValidationError type
When event.detail.error is not null on a change event, it has this shape:
interface ValidationError {
code: string; // machine-readable error code
message: string; // human-readable error message
}
ErrorCode values
The following values are possible for event.detail.code on the error event:
| Code | Category | Description |
|---|---|---|
MOUNT_ERROR | Infrastructure | Failed to mount the element iframe |
POSTMESSAGE_TIMEOUT | Infrastructure | PostMessage did not receive a response within timeoutMs |
IFRAME_LOAD_ERROR | Infrastructure | Element iframe failed to load |
INITIALIZATION_ERROR | Infrastructure | SDK failed to initialize |
API_ERROR | API | Basis Theory API returned an error response |
NETWORK_ERROR | API | Network request failed (no response received) |
VALIDATION_ERROR | Client | Input failed validation before an API request was made |
INVALID_CONFIGURATION | Client | Invalid SDK or element options were provided |
UNKNOWN_ERROR | Client | An unexpected error occurred |
Examples
Track form validity across multiple elements
- Web Elements
- React Elements
const formState = {
cardNumber: { isValid: false },
expiry: { isValid: false },
cvv: { isValid: false },
};
function updateSubmitButton() {
const allValid = Object.values(formState).every((s) => s.isValid);
document.getElementById('submit').disabled = !allValid;
}
cardNumberEl.on('change', (event) => {
formState.cardNumber.isValid = event.detail.isValid;
if (event.detail.cardBrand) {
document.getElementById('card-brand').textContent = event.detail.cardBrand;
}
updateSubmitButton();
});
expiryEl.on('change', (event) => {
formState.expiry.isValid = event.detail.isValid;
updateSubmitButton();
});
cvvEl.on('change', (event) => {
formState.cvv.isValid = event.detail.isValid;
updateSubmitButton();
});
import { useState } from 'react';
import {
CardNumberElement,
ExpiryElement,
CVVElement,
} from '@basis-theory/react-elements';
function PaymentForm() {
const [validity, setValidity] = useState({
cardNumber: false,
expiry: false,
cvv: false,
});
const [cardBrand, setCardBrand] = useState(null);
const isFormValid = Object.values(validity).every(Boolean);
return (
<form>
<CardNumberElement
id="card-number"
onChange={(event) => {
setValidity((prev) => ({ ...prev, cardNumber: event.detail.isValid }));
if (event.detail.cardBrand) setCardBrand(event.detail.cardBrand);
}}
/>
{cardBrand && <span>{cardBrand}</span>}
<ExpiryElement
id="expiry"
onChange={(event) =>
setValidity((prev) => ({ ...prev, expiry: event.detail.isValid }))
}
/>
<CVVElement
id="cvv"
onChange={(event) =>
setValidity((prev) => ({ ...prev, cvv: event.detail.isValid }))
}
/>
<button type="submit" disabled={!isFormValid}>
Pay Now
</button>
</form>
);
}
Handle errors
cardNumberEl.on('error', (event) => {
const { code, message, status } = event.detail;
switch (code) {
case 'MOUNT_ERROR':
case 'IFRAME_LOAD_ERROR':
showError('Could not load the payment form. Please refresh and try again.');
break;
case 'POSTMESSAGE_TIMEOUT':
showError('Request timed out. Check your network connection.');
break;
case 'API_ERROR':
if (status === 400) {
showError('Invalid card details. Please check and try again.');
} else {
showError(`API error ${status}: ${message}`);
}
break;
default:
showError(message);
}
});
Show loading state until all elements are ready
- Web Elements
- React Elements
const ready = { cardNumber: false, expiry: false, cvv: false };
function checkAllReady() {
if (Object.values(ready).every(Boolean)) {
document.getElementById('loading').style.display = 'none';
document.getElementById('form').style.display = 'block';
}
}
cardNumberEl.on('ready', () => { ready.cardNumber = true; checkAllReady(); });
expiryEl.on('ready', () => { ready.expiry = true; checkAllReady(); });
cvvEl.on('ready', () => { ready.cvv = true; checkAllReady(); });
import { useState } from 'react';
import { CardNumberElement, ExpiryElement, CVVElement } from '@basis-theory/react-elements';
function PaymentForm() {
const [ready, setReady] = useState({ cardNumber: false, expiry: false, cvv: false });
const allReady = Object.values(ready).every(Boolean);
return (
<div>
{!allReady && <div className="loading-spinner" />}
<div style={{ visibility: allReady ? 'visible' : 'hidden' }}>
<CardNumberElement id="card-number" onReady={() => setReady((r) => ({ ...r, cardNumber: true }))} />
<ExpiryElement id="expiry" onReady={() => setReady((r) => ({ ...r, expiry: true }))} />
<CVVElement id="cvv" onReady={() => setReady((r) => ({ ...r, cvv: true }))} />
</div>
</div>
);
}