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
| Event | Fires when | Typical use |
|---|---|---|
ready | The element has finished rendering and is interactive. | Hide your loading spinner / skeleton; mark the form ready. |
change | The element's value changes (and on unmount, with complete: false). | Enable the Pay button when complete; surface error for inline validation. |
focus | The field gains focus. | Apply a focus ring or floating-label state on your wrapper. |
blur | The 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.
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.)
Related
- Customize the look & feel — styling, themes, the
styleallowlist - Elements reference — per-element options + collect shapes
- React — the same events via
useVora()/ component props - Standalone wallets — the
express-checkoutelement