Skip to main content

Element events

Every element exposes the same event API — wire your checkout UI to it for loading states, button enablement, validation messages, and focus styling. It's binder-agnostic: the events and payloads are identical on every gateway, so your code never branches on which binder backs your account.

The API — on / off

const card = elements.create("card", {});

const handler = (e) => { /* … */ };
card.on("change", handler); // subscribe
card.off("change", handler); // unsubscribe (pass the same handler reference)

card.mount("#card-element");

on(event, handler) and off(event, handler) are on every element type (card, email, cardholder, address, save-for-future-use, express-checkout, …).

The four events

EventFires whenTypical use
readyThe element has finished rendering and is interactive.Hide your loading spinner / skeleton; mark the form ready.
changeThe element's value changes (and on unmount, with complete: false).Enable the Pay button when complete; surface error for inline validation.
focusThe field gains focus.Apply a focus ring or floating-label state on your wrapper.
blurThe field loses focus (or, on the express-checkout element, the buyer dismisses the wallet sheet).Validate on blur; reset focus styling.

All four fire with the same payload:

interface ElementChangeEvent {
complete: boolean; // value passes its own validation and is non-empty (or empty + not required)
error?: { code: string; message: string }; // scrubbed validation error, undefined when valid
}

Gate the submit button + show validation

const card = elements.create("card", {});

card.on("change", (e) => {
payButton.disabled = !e.complete; // enable only when valid
showFieldError(e.error ? e.error.message : null); // inline validation
});

card.on("ready", () => hideSpinner());
card.on("focus", () => cardWrapper.classList.add("focused"));
card.on("blur", () => cardWrapper.classList.remove("focused"));

card.mount("#card-element");

error.message is already scrubbed for display; error.code is the stable identifier to branch on.

Wallet buttons (express-checkout) use the same events

The express-checkout element (standalone wallets) maps the native wallet sheet's lifecycle onto these same four events — so you wire it the same way. When the buyer authorizes in the wallet sheet, it fires change with complete: true; read the resulting vp_pmt_* by calling submit() on that collection:

express.on("change", async (e) => {
if (e.error) return; // buttons unavailable / declined
if (e.complete) {
const result = await walletCollection.submit();
if (result.token) await chargeOnYourServer(result.token, result.wallet);
}
});

(If the buyer dismisses the sheet, you get blur; if it can't render, you get change with an error.)

Localization (locale)

The SDK accepts a BCP-47 locale — on the client and per element:

const vora = new window.Vora({
publishableKey: "vp_pk_test_…",
locale: "fr-FR", // client-wide default
});

const card = elements.create("card", { locale: "de-DE" }); // per-element override

It's meant to drive field labels, validation messages, and error strings.

English-only today

The locale value is accepted, recorded, and forwarded, but the default label / validation surface is English-only right now — the value is captured so localization can light up in a future release without an integration change. Set it if you want to be forward-compatible; just don't expect translated strings yet. (Some binder-served field text may already reflect the locale where the underlying field provider localizes.)