Easy Labs
SDKsReact

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

ElementUse forRef 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

src/CombinedCard.tsx
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

src/SplitCard.tsx
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)

src/BankAccount.tsx
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 accept style and className props that apply to that wrapper.
  • The input inside the iframe is themed with the style prop 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. — neither cardElement nor 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() returned undefined.
  • 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,
});

On this page