Skip to main content

Element Types

v3 provides seven element types. Each renders as a sandboxed iframe. The combined card element and the four individual input elements share the same core interface; copyButton is an action button and cardDisplay is a read-only view of a stored card, each with its own options and events.

TypecreateElement keyDescription
Card'card'Combined card input — number, expiry, and CVV in a single iframe
Card Number'cardNumber'PAN input with Luhn validation and brand detection
Expiry'expiry'MM/YY expiration date input
CVV'cvv'Card verification code input
Text'text'General-purpose single-line text input
Copy Button'copyButton'Secure button that copies an element's value to the clipboard
Card Display'cardDisplay'Read-only display of a stored card, loaded from a token ID

Shared Options​

The five input elements — card, cardNumber, expiry, cvv and text — accept these options in createElement and element.update(). copyButton is a button, not an input, and takes its own options instead:

OptionTypeDefaultDescription
placeholderstring—Placeholder text shown when the input is empty
ariaLabelstringElement-specificARIA label for screen readers
disabledbooleanfalseDisables the input
readOnlybooleanfalseMakes the input read-only
enterKeyHint'enter' | 'done' | 'go' | 'next' | 'previous' | 'search' | 'send'Browser defaultAction label rendered on the enter key of mobile virtual keyboards

enterKeyHint​

enterKeyHint maps to the HTML enterkeyhint attribute. Because the input lives inside the Basis Theory iframe, this option is the way to set it — use 'next' on the fields a user moves through and 'done' on the last one, so the soft keyboard offers a way forward instead of a generic action key.

const cardNumberEl = bt.createElement('cardNumber', { enterKeyHint: 'next' });
const expiryEl = bt.createElement('expiry', { enterKeyHint: 'next' });
const cvvEl = bt.createElement('cvv', { enterKeyHint: 'done' });

The attribute changes what the enter key shows, not what pressing it does. A value outside the seven above throws a ConfigurationError from createElement().

Shared Methods​

Every element exposes the same interface:

MethodSignatureDescription
mount(selector: string | HTMLElement) => Promise<void>Attaches the iframe to the DOM; resolves when the element is interactive
unmount() => voidRemoves the iframe from the DOM and clears cached values
update(options: Partial<ElementOptions>) => Promise<void>Updates options on a mounted element
focus() => voidFocuses the input
blur() => voidBlurs the input
clear() => voidClears the current value
on(event, listener) => () => voidSubscribes to an event; returns an unsubscribe function

Properties:

PropertyTypeDescription
idstringUnique element identifier (read-only)
typeElementTypeElement type string (read-only)
mountedbooleanWhether the element is currently mounted (read-only)

Events:

EventFires when…
readyThe iframe has loaded and the element is interactive
changeThe user types or the value changes
focusThe input gains focus
blurThe input loses focus
errorAn infrastructure or API error occurs

See Events for full payload shapes.


card​

Collects the card number, expiration date, and CVV together in a single iframe. Use it when you want one compact, pre-styled card input instead of mounting cardNumber, expiry, and cvv separately. The three sub-fields are exposed directly on the element — card.number, card.expiryDate, and card.cvc — for tokenization.

const cardEl = bt.createElement('card', {
placeholder: {
cardNumber: '4242 4242 4242 4242',
expiryDate: 'MM/YY',
cvc: 'CVC',
},
layout: 'auto',
iconPosition: 'left',
// Move through the fields, finish on the CVV
enterKeyHint: { cardNumber: 'next', expiryDate: 'next', cvv: 'done' },
});

await cardEl.mount('#card-container');

card options​

OptionTypeDefaultDescription
placeholder{ cardNumber?: string; expiryDate?: string; cvc?: string }—Placeholder text per sub-field. Unlike other elements, this is a structured object, not a string.
ariaLabelstring'Card Input'ARIA label for the card input
disabledbooleanfalseDisables all three sub-fields
readOnlybooleanfalseMakes all three sub-fields read-only
enterKeyHintEnterKeyHint | { cardNumber?: EnterKeyHint; expiryDate?: EnterKeyHint; cvv?: EnterKeyHint; cvc?: EnterKeyHint }Browser defaultEnter key label on mobile keyboards. A single value applies to all three sub-fields; an object sets each one independently. Each value replaces the whole set — a sub-field left out of an update() has its hint cleared. The CVV field also accepts the key cvc; cvv wins if both are set
layout'row' | 'column' | 'auto''auto'Field arrangement. 'auto' stacks the fields vertically when the container is narrower than stackAt
stackAtnumber400Container width (px) below which 'auto' layout stacks vertically. Only applies when layout: 'auto'
iconPosition'left' | 'right' | 'none''left'Position of the card brand icon, or 'none' to hide it
cardBrandsstring[]—Restricts the card brands the input will accept
agentToolsbooleanfalseRegisters a WebMCP enter_card tool so a browser agent can fill in the combined card element. Set it at creation. See the agent checkout guide for existing Elements and sidecar integrations, and Agent Tools (WebMCP) for the tool contract.
cvcRequiredbooleantrueWhen false, an empty CVC counts as complete; a partly entered CVC remains invalid. Set it at creation (changing it with update() throws a ConfigurationError). Confirm your payment flow permits omitting CVC before disabling the requirement.
binLookupboolean | { enabled?: boolean; debounceMs?: number }—Enables BIN enrichment, adding binDetails to the change event. Setting coBadge overrides this — enrichment stays on even with binLookup: false
coBadge{ preferredNetworks?: string[]; mode?: 'auto' | 'manual' }—Co-badged card network support. Forces binLookup on, overriding an explicit binLookup: false

Sub-field accessors​

The card element renders three sub-fields inside one iframe. To tokenize, reference these sub-fields directly on the element rather than passing the card element itself:

AccessorSub-field
card.numberCard number
card.expiryDateExpiration date (resolves both month and year)
card.cvcCVV

These are sub-field references for tokenization only. They cannot be mounted, updated, or focused independently — call mount(), update(), focus(), etc. on the card element itself.

Tokenization​

Pass the sub-field references into bt.tokens.create. Use card.expiryDate for both expiration_month and expiration_year — passing the same reference to both is required, because the SDK parses the MM/YY value into a numeric month and four-digit year inside the secure element before the request is sent:

const token = await bt.tokens.create({
type: 'card',
data: {
number: cardEl.number,
expiration_month: cardEl.expiryDate,
expiration_year: cardEl.expiryDate,
cvc: cardEl.cvc,
},
});

// Send token.id to your backend to charge the card
console.log('Token created:', token.id);

cardNumber​

Collects the card PAN. Validates format and Luhn checksum, and detects the card brand as the user types.

const cardNumberEl = bt.createElement('cardNumber', {
placeholder: '4242 4242 4242 4242',
ariaLabel: 'Card number',
});

await cardNumberEl.mount('#card-number-container');

change event​

The cardNumber element emits additional fields on its change event beyond the shared ChangeEventDetail:

FieldTypeDescription
cardBrandstringDetected brand (e.g. 'visa', 'mastercard', 'amex')
last4string | nullLast 4 digits — safe to display
binstring | nullBIN (first 6–8 digits)
cvvLengthsnumber[] | nullExpected CVV length(s) for detected brand
potentialBrandsstring[]All brands that could match at the current input length
matchStrengthnumberBrand detection confidence (0–1)
networksstring[]Networks a co-badged card supports. Present only with BIN lookup or co-badge enabled — see Co-Badge Support.
selectedNetworkstringSelected network on a co-badged card — see Co-Badge Support.
cardNumberEl.on('change', (event) => {
const { isValid, cardBrand, last4, bin } = event.detail;
console.log(`${cardBrand} ...${last4}`, isValid);
});

Co-brand detection​

Some card numbers match more than one brand (for example, a card that could be Mastercard or Maestro). The SDK detects this automatically and exposes potentialBrands on the change event. When potentialBrands.length > 1, you can prompt the user to pick their preferred brand:

cardNumberEl.on('change', (event) => {
const { cardBrand, potentialBrands, matchStrength } = event.detail;

if (potentialBrands && potentialBrands.length > 1) {
// Show brand picker UI with potentialBrands options
showBrandPicker(potentialBrands);
} else {
hideBrandPicker();
// cardBrand is the detected brand (e.g. 'mastercard')
}
});

matchStrength (0–1) indicates how confident the detection is — useful for deciding when to show or hide UI. A value approaching 1 means only one brand matches.

BIN enrichment​

cardNumber and card can look up the card's BIN (its first six digits) and report what the issuer says about it. Enrichment adds a binDetails object to the change event carrying the card brand, funding type, issuing country, issuing bank and issuer segment.

Turn it on with binLookup:

Option valueBehavior
(omitted)Enrichment is off — unless coBadge is set, which turns it on automatically. When off, only locally detected fields (cardBrand, last4, bin) are emitted.
trueEnables enrichment with the default debounce (350 ms).
{ enabled, debounceMs }Enables enrichment and lets you tune it. debounceMs controls how long after the user stops typing before the lookup runs.
false or { enabled: false }Disables enrichment. Overridden by coBadge — if coBadge is set, enrichment stays on regardless.
const cardNumberEl = bt.createElement('cardNumber', {
binLookup: true,
});

cardNumberEl.on('change', (event) => {
const { binDetails } = event.detail;
if (binDetails?.type === 'prepaid') {
showPrepaidNotice();
}
});

await cardNumberEl.mount('#card-number-container');

The lookup is debounced and asynchronous, so binDetails appears shortly after the sixth digit rather than with it. See BinDetails for the full shape and its caveats.

Co-badged cards​

Enrichment also detects co-badged cards — cards carrying more than one payment network, for example a domestic network alongside Visa or Mastercard. Those surface as networks and selectedNetwork on the change event, with a networkChanged event when the selection changes. Configure this with coBadge, which enables enrichment on its own; see Co-Badge Support.

React ref​

CardNumberElementRef extends the base ref with convenience state:

PropertyTypeDescription
completebooleanInput is valid and complete
emptybooleanInput is empty
validbooleanInput passes validation
cardBrandstringDetected card brand
last4stringLast 4 digits
binstringBIN
networksstring[]Networks a co-badged card supports
selectedNetworkstringCurrently selected network
binDetailsBinDetailsIssuer metadata from BIN enrichment

expiry​

Collects the card expiration date in MM/YY format. Validates that the date is not in the past.

const expiryEl = bt.createElement('expiry', {
placeholder: 'MM/YY',
});

await expiryEl.mount('#expiry-container');

The expiry element accepts only the shared options. When used in tokenization, pass the same element reference for both expiration_month and expiration_year — passing the same reference to both is required, because the SDK parses the MM/YY value into a numeric month and four-digit year inside the secure element before the request is sent:

bt.tokens.create({
type: 'card',
data: {
number: cardNumberEl,
expiration_month: expiryEl,
expiration_year: expiryEl,
cvc: cvvEl,
},
});

cvv​

Collects the card verification code (CVV/CVC). When a cardNumber element is present on the same page, the SDK automatically updates the CVV element's expected length to match the detected card brand (3 digits for Visa/Mastercard, 4 for Amex).

const cvvEl = bt.createElement('cvv', {
placeholder: '•••',
showToggle: true, // renders a show/hide button inside the field
});

await cvvEl.mount('#cvv-container');

CVV-specific options​

OptionTypeDefaultDescription
concealbooleantrueHides the CVV value. Set to false to render it visible. Can be changed at runtime with update().
showTogglebooleanfalseRenders a built-in show/hide toggle inside the field. Set at creation — cannot be changed via update().

Controlling CVV visibility​

The CVV is masked by default. Use conceal to drive visibility from your own control on the page, or to render the CVV visible from the start:

let concealed = true;
const cvvEl = bt.createElement('cvv', { conceal: concealed });

await cvvEl.mount('#cvv-container');

revealButton.addEventListener('click', async () => {
concealed = !concealed;
await cvvEl.update({ conceal: concealed });
});

conceal and showToggle work together: conceal sets the state, the built-in toggle flips it from there, and a later update({ conceal }) overrides the user's last toggle. Updates that don't include conceal leave the current visibility unchanged.

A visible CVV can be read by anyone looking at the screen and captured by screen recording on your page. The value still never leaves the Basis Theory iframe.

text​

General-purpose single-line text input. Use it to collect any non-card sensitive value (SSN, account number, routing number, etc.) without it touching your servers.

const ssnEl = bt.createElement('text', {
placeholder: '•••-••-••••',
mask: [/\d/, /\d/, /\d/, '-', /\d/, /\d/, '-', /\d/, /\d/, /\d/, /\d/],
transform: [/-/g, ''], // strip dashes before tokenization
validation: /^\d{9}$/, // must be exactly 9 digits after transform
});

await ssnEl.mount('#ssn-container');

Text-specific options​

OptionTypeMutable via update()Description
validationRegExpNoPattern the value must match to be considered valid
requiredbooleanYesWhether the field is required
maxLengthnumberYesMaximum character length
passwordbooleanYesRenders as a password field (type="password")
inputModestringYesMobile keyboard hint ('numeric', 'tel', 'email', etc.)
mask(RegExp | string)[]NoCharacter-by-character input mask. Each position is a RegExp that the typed character must match, or a literal string character inserted automatically
transform[RegExp, string]NoApplied to the value before tokenization: value.replace(pattern, replacement). Does not affect what the user sees
validation, mask, and transform define the data contract for the element and are immutable after creation. To change them, unmount and recreate the element.

Mask example​

// Phone: (555) 123-4567
const phoneEl = bt.createElement('text', {
placeholder: '(555) 123-4567',
mask: [
'(', /\d/, /\d/, /\d/, ')',
' ', /\d/, /\d/, /\d/,
'-', /\d/, /\d/, /\d/, /\d/,
],
transform: [/\D/g, ''], // strip non-digits → "5551234567" is tokenized
});

copyButton​

Renders a secure button that copies a value to the clipboard — either a static string or the live value of another element (for example, mirroring a cardNumber). Unlike the input elements, copyButton has no validation and emits no change event; it emits a copy event when clicked.

copyButton requires allowClipboard: true. Omitting it throws a ConfigurationError. Copying raw card data to the clipboard may fall outside your PCI-DSS scope — only enable this once you have confirmed clipboard access is acceptable for the data being copied.
const copyButton = bt.createElement('copyButton', {
allowClipboard: true, // required — omitting throws ConfigurationError
text: 'Copy card number',
});

await copyButton.mount('#copy-button-container');

// Link the button to another element's live value
await copyButton.setValueRef(cardNumberEl);

copyButton.on('copy', (event) => {
if (event.detail.success) {
console.log('Copied to clipboard');
}
});

copyButton options​

OptionTypeDefaultDescription
allowClipboardtrue—Required. Explicit opt-in to clipboard access. Must be true — any other value (including omitting it) throws ConfigurationError.
valuestring—Static string to copy when the button is clicked. Overridden by setValueRef() if called.
textstring'Copy'Button label.
disabledbooleanfalseDisables the button.

copyButton does not accept the shared input options (placeholder, ariaLabel, readOnly, enterKeyHint).

setValueRef​

setValueRef(element) links the copy button to another element's live value. After linking, clicking the button copies the current value of the referenced element — that value never crosses into your page. Use it to let a user copy a cardNumber (or any other element) they are viewing, without exposing the raw value to your application.

const cardNumberEl = bt.createElement('cardNumber');
const copyButton = bt.createElement('copyButton', { allowClipboard: true });

await Promise.all([
cardNumberEl.mount('#card-number'),
copyButton.mount('#copy-button'),
]);

await copyButton.setValueRef(cardNumberEl);
MethodSignatureDescription
setValueRef(element: Element) => Promise<void>Links the button to another element's live value. A linked reference takes precedence over the static value option. Throws if the button is not mounted or the passed element has no ID.

In React, call setValueRef on the element ref (see the example above).

copy event​

copyButton emits a copy event each time it is clicked, reporting whether the copy succeeded. In React, subscribe with the onCopy prop. See Events for the full payload.

cardDisplay​

Renders a stored card back to its cardholder, loaded from a token ID. Use it for account pages, virtual card screens, or anywhere a saved card has to be readable without the card data entering your application. Unlike the input elements, cardDisplay is read-only — no validation, no focus or blur events, and focus(), blur(), and clear() are no-ops.

The element reads the card with a session that your backend authorizes, so it requires sessionAuthorizationUrl at initialization (see Authorizing the display session):

const bt = BasisTheory('<PUBLIC_API_KEY>', {
sessionAuthorizationUrl: 'https://api.your-app.com/authorize-session',
});
const cardDisplay = bt.createElement('cardDisplay', {
tokenId: '<TOKEN_ID>',
number: { display: 'masked' },
expiration: { display: 'visible' },
cvc: { display: 'hidden' },
});

cardDisplay.on('change', (event) => {
if (event.detail.loaded) {
console.log(`${event.detail.brand} ending in ${event.detail.last4}`);
}
});

cardDisplay.on('error', (event) => {
console.error(event.detail.code, event.detail.message);
});

await cardDisplay.mount('#card-display-container');

cardDisplay options​

OptionTypeDefaultDescription
tokenIdstring—Required. ID of the card token to display. A missing or non-string value raises ConfigurationError. Passing a new value to update() clears the panel and loads that token.
number{ display: DisplayMode; label?: string | false }{ display: 'masked' }Display mode and optional label override for the card number.
expiration{ display: DisplayMode; label?: string | false }{ display: 'masked' }Display mode and optional label override for the expiration date.
cvc{ display: DisplayMode; label?: string | false }{ display: 'masked' }Display mode and optional label override for the CVC. Omitted when the token has no cvc.
layout'stacked' | 'inline''stacked'Arrangement of the fields. See Presentation options.
fieldOrder('number' | 'expiration' | 'cvc')[]['number', 'expiration', 'cvc']Display order of the fields. Fields you omit keep their default relative order after the ones you list.
showTogglebooleantrueWhether the built-in reveal control appears when a field is masked. Unlike the CVV element's showToggle, this defaults to true.
allowClipboardbooleanfalseShows a copy button on each visible field. Also requires allowClipboard: true on BasisTheory() initialization.

Each field takes its own DisplayMode:

ModeRenders
'masked'Dots, plus the last four digits for the card number
'visible'The value in plaintext
'hidden'Nothing — the field is removed from the layout

The reveal toggle appears only while a field is masked, and switches every masked field at once — set showToggle: false to remove it in favor of your own control. The number renders in groups of four digits and the expiration as MM/YY; neither format is configurable. Labels, spacing, and border radius follow the theme tokens, but the element paints no background of its own, so the panel takes the background of its container.

Each field's copy button copies the stored value rather than the rendered one — unformatted digits, MMYY, and the raw CVC — and works on masked fields.

cardDisplay does not accept the shared input options (placeholder, ariaLabel, disabled, readOnly).

Presentation options​

By default the panel stacks each field on its own row, with the built-in English labels ("CARD NUMBER", "EXPIRATION", "CVC") and reveal toggle. Every option below defaults to that behavior, so existing integrations don't change unless you opt in.

const bt = BasisTheory('<PUBLIC_API_KEY>', {
sessionAuthorizationUrl: 'https://api.your-app.com/authorize-session',
allowClipboard: true, // required — the allowClipboard example below also needs this
});

const cardDisplay = bt.createElement('cardDisplay', {
tokenId: '<TOKEN_ID>',
layout: 'inline',
fieldOrder: ['expiration', 'number', 'cvc'],
number: { display: 'masked', label: 'Card number' },
expiration: { display: 'visible', label: false },
showToggle: false,
allowClipboard: true,
});
  • layout: 'inline' lays the fields out in a row instead of stacked. A long card number wraps onto a second line rather than being cut off or overlapping the next field; the copy button stays next to it.
  • label overrides a field's default text. It is rendered as plain text, never as HTML, and capped at 100 characters. Pass false to hide the label entirely.
  • showToggle: false removes the element's own reveal control — use it when you want to drive reveal from your own UI via update() instead of the built-in toggle.
  • allowClipboard: true shows a copy button on every visible field, mirroring copyButton's own opt-in — it also requires allowClipboard: true on BasisTheory() initialization, or createElement() throws ConfigurationError.

Loading and events​

mount() resolves when the iframe is ready, which is before the card data arrives. The token load runs after it, so a session or token failure surfaces as an error event rather than a rejected mount().

EventFires when…event.detail
readyThe iframe has loaded and received its configurationelementType, elementId, timestamp
changeCard data has loaded into the elementloaded, brand, last4
errorThe session or token load failedcode, message
resizeThe element's rendered height changesheight

change carries the brand and last four digits only; the card data stays inside the iframe. The element also exposes a loaded property. Failure codes are listed in Troubleshooting.

cardDisplay measures its own content and resizes its iframe automatically, so you don't need to apply the height yourself — even as fields are toggled between display modes or the layout changes. Listen for resize/onResize only if your own page needs to react to the new height, such as animating a container around the element.

Authorizing the display session​

The element loads a token in four steps:

  1. The SDK creates a session inside the Basis Theory iframe and receives a nonce. Your application never calls sessions.create itself.
  2. Your page POSTs {"nonce": "<NONCE>"} as JSON to sessionAuthorizationUrl, with a 30-second timeout.
  3. Your backend authorizes the nonce with a Private Application key and returns any 2xx response. The SDK reads the status code and ignores the body.
  4. The SDK retrieves the token with the authorized session key inside the iframe. The session key never reaches your page.

Your endpoint receives the nonce and nothing else — no token ID, nothing identifying the user. It must:

  • Authenticate the caller and reject unauthenticated requests, then pick the token from that identity rather than from the request body, which any caller controls.
  • Share your page's origin. The SDK leaves credentials unset and sends no headers you control, so cookies reach it only when the origins match. A cross-origin endpoint must also allow CORS, which is a browser check rather than authentication.
  • Protect against CSRF when it authenticates with cookies. The caller supplies the nonce, so an attacker can have a logged-in victim's browser authorize the attacker's session against the victim's card. A CSRF token or SameSite=Strict closes that path.
backend.js
const express = require("express");
const { BasisTheoryClient } = require("@basis-theory/node-sdk");

const app = express();

app.use(express.json());
app.use(requireAuthenticatedUser); // your middleware; must reject anonymous requests

app.post("/authorize-session", async (request, response) => {
const bt = new BasisTheoryClient({ apiKey: "<PRIVATE_API_KEY>" });
const { nonce } = request.body;
const tokenId = await getCardTokenIdForUser(request.user); // your own lookup

await bt.sessions.authorize({
nonce,
rules: [
{
description: "Display card",
priority: 1,
conditions: [
{ attribute: "id", operator: "equals", value: tokenId },
],
permissions: ["token:read"],
transform: "reveal",
},
],
});

response.sendStatus(204);
});
The reveal transform returns plaintext card data to the session. Scope the rule to the one token the signed-in user may see. A container-scoped rule lets any nonce from your page reveal every token in that container.

The token must be type card with a number in its data; anything else fails the load with INVALID_TOKEN_TYPE or INVALID_TOKEN_DATA. The expiration renders from expiration_month and expiration_year.


For visual customization across all elements (colors, typography, spacing, border radius), see Theming.