Elements
Pre-built form elements for collecting payment information securely.
Elements are iframe-isolated form inputs for collecting card and bank-account details. The raw values you type into them are entered into a Basis Theory iframe served from a different origin than your app — they never enter your React state, your DOM, or your network requests, which keeps your integration out of PCI scope.
@easylabs/react re-exports five elements from @basis-theory/react-elements. Use them inside an EasyProvider, attach a ref, and pass that ref to useEasy().checkout(), createPaymentInstrument(), or tokenizePaymentInstrument() — the SDK handles tokenization in one step.
Available elements
| Element | Use for | Ref type |
|---|---|---|
<CardElement> | A single combined card input (number, expiry, CVC). | ICardElement |
<CardNumberElement> | Card number only, when you want a split layout. | ICardNumberElement |
<CardExpirationDateElement> | Expiry date only. | ICardExpirationDateElement |
<CardVerificationCodeElement> | CVC / CVV only. | ICardVerificationCodeElement |
<TextElement> | Generic text input — used by the SDK for ACH routing and account numbers. | ITextElement |
Either cardElement or the trio (cardNumberElement + cardExpirationDateElement + cardVerificationCodeElement) is required for card tokenization, never both. For bank accounts, both routingElement and accountElement are required.
Combined card
import { CardElement, useEasy, type ICardElement } from "@easylabs/react";
import { useRef } from "react";
export function CombinedCard({ customerId }: { customerId: string }) {
const cardRef = useRef<ICardElement>(null);
const { createPaymentInstrument } = useEasy();
async function save() {
const res = await createPaymentInstrument({
type: "PAYMENT_CARD",
customerId,
name: "Primary card",
cardElement: cardRef,
});
console.log("instrument", res.data.id);
}
return (
<>
<CardElement ref={cardRef} id="card" />
<button onClick={save}>Save card</button>
</>
);
}Split card fields
import {
CardNumberElement,
CardExpirationDateElement,
CardVerificationCodeElement,
useEasy,
type ICardNumberElement,
type ICardExpirationDateElement,
type ICardVerificationCodeElement,
} from "@easylabs/react";
import { useRef } from "react";
export function SplitCard({ customerId }: { customerId: string }) {
const numberRef = useRef<ICardNumberElement>(null);
const expRef = useRef<ICardExpirationDateElement>(null);
const cvcRef = useRef<ICardVerificationCodeElement>(null);
const { createPaymentInstrument } = useEasy();
async function save() {
await createPaymentInstrument({
type: "PAYMENT_CARD",
customerId,
name: "Primary card",
cardNumberElement: numberRef,
cardExpirationDateElement: expRef,
cardVerificationCodeElement: cvcRef,
});
}
return (
<>
<CardNumberElement ref={numberRef} id="card-number" />
<CardExpirationDateElement ref={expRef} id="card-exp" />
<CardVerificationCodeElement ref={cvcRef} id="card-cvc" />
<button onClick={save}>Save card</button>
</>
);
}Bank account (ACH)
import { TextElement, useEasy, type ITextElement } from "@easylabs/react";
import { useRef } from "react";
export function BankAccount({ customerId }: { customerId: string }) {
const routingRef = useRef<ITextElement>(null);
const accountRef = useRef<ITextElement>(null);
const { createPaymentInstrument } = useEasy();
async function save() {
await createPaymentInstrument({
type: "BANK_ACCOUNT",
customerId,
name: "Checking account",
accountType: "PERSONAL_CHECKING",
routingElement: routingRef,
accountElement: accountRef,
});
}
return (
<>
<label htmlFor="routing">Routing number</label>
<TextElement ref={routingRef} id="routing" placeholder="Routing" />
<label htmlFor="account">Account number</label>
<TextElement ref={accountRef} id="account" placeholder="Account" />
<button onClick={save}>Save bank account</button>
</>
);
}Styling
Elements live inside cross-origin iframes, so your page CSS does not reach the input itself. There are two layers of styling:
- The mount container is a regular DOM node you can target with normal CSS, layout primitives, and design-system wrappers. Wrap the element in your own
<div>to control border, padding, focus ring, etc. Element components also acceptstyleandclassNameprops that apply to that wrapper. - The input inside the iframe is themed with the
styleprop on each element, which accepts the Basis Theory element style schema. Use it for font, color, and placeholder color of the actual input.
<CardElement
ref={cardRef}
id="card"
style={{
base: {
color: "#0f172a",
fontFamily: "Inter, sans-serif",
fontSize: "16px",
"::placeholder": { color: "#94a3b8" },
},
invalid: { color: "#ef4444" },
}}
/>For autofill, pass autoComplete on CardElement:
<CardElement
ref={cardRef}
id="card"
autoComplete={{ number: "cc-number", expirationDate: "cc-exp", csc: "cc-csc" }}
/>Validation
Validation runs inside the iframe and surfaces through Basis Theory element events (change, ready, blur, focus). Subscribe with the ref:
useEffect(() => {
const el = cardRef.current;
if (!el) return;
const sub = el.on("change", (event) => {
setError(event.errors?.[0]?.type ?? null);
});
return () => sub.unsubscribe();
}, []);If you call checkout or createPaymentInstrument with an incomplete element, the SDK throws before hitting the API:
Card element reference is not set.— neithercardElementnor the split trio is mounted.All card elements must be available for separate element tokenization.— partial split-card refs.Card expiration date is incomplete.—cardExpirationDateElement.month()or.year()returnedundefined.Routing or account element reference is not set— bank account ref missing.
For the full event API (change, ready, blur, focus, mask events) see the Basis Theory React Elements docs.
Submitting
The recommended path is to skip manual tokenization — pass the element refs straight into checkout or createPaymentInstrument and the SDK does the right thing:
const res = await checkout({
customer_creation: true,
customer_details: { first_name: "Ada", last_name: "Lovelace", email: "ada@example.com" },
line_items: [{ price_id: "price_123", quantity: 1 }],
source: { type: "PAYMENT_CARD", name: "Ada Lovelace", cardElement: cardRef },
});If you need a bare token (e.g. to store it for later use without creating an instrument), use tokenizePaymentInstrument:
const tokenId = await tokenizePaymentInstrument({
type: "PAYMENT_CARD",
cardElement: cardRef,
});