Skip to main content

Embedded Fields — Element reference

The catalog of UI elements you can create with vora.elements.create(): the options each accepts, the events it fires, and the value it contributes to the collection's submit() result. In the default "embed" render mode the card element is one iframe that renders the entire payment surface — card fields plus, when the buyer is eligible, Apple Pay / Google Pay — and only email and save-for-future-use are placed separately; in "elements" mode you place cardholder, address and the others as their own fields, and the card is either the combined card box or the three discrete card-number / card-expiry / card-cvc fields. Which mode renders which element, the per-binder capability matrix and the silent fallback: Render modes: embed vs elements; the elements-mode walkthrough: Custom checkout.

Status overview​

Every kind you can pass to vora.elements.create(kind, options):

create() kindWhat it is
cardThe combined card iframe (number + expiry + CVC in one box) — your payment surface. See card.
card-numberDiscrete PAN field — mount independently in "elements" mode. Adds showIcon. See Discrete card fields.
card-expiryDiscrete expiry (MM / YY) field. See Discrete card fields.
card-cvcDiscrete security-code (CVC) field. See Discrete card fields.
emailVORA-rendered email input with validation. See email.
addressBilling-address form rendered in your page ("elements" mode). See cardholder and address.
cardholder"Name on card" text input rendered in your page ("elements" mode). See cardholder and address.
save-for-future-useCompliance-locked opt-in checkbox. See save-for-future-use.
saved-methodsDiscrete saved-card picker for elements mode. It renders its unavailable state for every account — the saved-card read surface behind it is not enabled — never an empty list, so do not build a returning-buyer flow on it. Returning buyers see their saved cards inside the card mount in embed mode.
express-checkoutStandalone Apple Pay / Google Pay wallet button row. Use this only when you create the session with integrationMode: "elements" (discrete rendering) — in the default "embed" mode the wallet buttons render inside the card mount instead, with no separate element to add. See Standalone wallets.
payment-method-pickerMethod-selector button row. Surfaces the buyer's choice on submit() — it does not tokenize. See payment-method-picker.

The Apple Pay / Google Pay wallet button is the express-checkout element; Standalone wallets covers it. There is no wallet-* element.

Cardholder name and billing address are collected inside the card mount in the default "embed" integration mode, so you don't mount separate elements for them. If you create the session with integrationMode: "elements" (and your account's gateway supports discrete rendering), cardholder and address mount as standalone fields instead; see Custom checkout (Elements mode).


Apple Pay & Google Pay — built into the card mount​

In the default "embed" integration mode, when the buyer's device supports a wallet (Apple Pay on Safari / iOS, Google Pay on Chrome with a saved card) and your domain is verified with each wallet network, the wallet button appears automatically as part of the card mount. There's no separate element to add — elements.create('card', …) is the only thing your integration needs. (In integrationMode: "elements" you instead mount the standalone express-checkout element for the wallet buttons.)

Placement inside the mount is decided by the gateway runtime so the buyer sees the same wallet UI they see on other checkouts — sometimes a row above the card inputs, sometimes a tab, sometimes a sheet that takes over the mount when tapped.

Eligibility gates (all must be true for the button to appear):

  1. The buyer's device + browser supports the wallet. Apple Pay needs Safari (macOS / iOS) or a Mac with Touch ID + paired iPhone. Google Pay needs Chrome, Edge, or any Chromium-based browser signed in to a Google account.
  2. The buyer has a payment method on file with the wallet.
  3. Your serving domain is registered with the wallet network for your active merchant account (new merchants are registered automatically on first session-create).

Failure-mode contract: if any gate fails, the wallet button doesn't appear and the buyer completes checkout via the card field. The button never appears pointing at the wrong destination, and nothing blocks the card field. (The wallet error codes and the wallet field on the result belong to the standalone express-checkout element in elements mode — the embed emits neither.)

Buyer flow when the button appears: the buyer taps the button, authenticates in the wallet sheet (Face ID, Touch ID, the Chrome dialog), and elements.submit() resolves with the same two arms as a card in embed mode — charged: true for a guest, or token for a buyer on the session. The tap has already moved the money on a charge-and-save session — a returned token is a vaulted card, not a handle to charge this payment again; do not call POST /v1/payment_intents for it. Issuer authentication is usually satisfied by the wallet's device attestation; when a challenge is required the embed runs it inside its own iframe. Branch as in SubmitResult shape, and confirm settlement via the webhook before fulfilling.

Apple Pay domain setup. Each domain that serves Apple Pay needs a one-time verification ceremony with Apple — host a small file at https://{your-domain}/.well-known/apple-developer-merchantid-domain-association and we trigger the verification call. Full walkthrough at Apple Pay setup, including framework-specific hosting recipes (Next.js, Vite, Vercel, Cloudflare, Nginx, Apache, Express) and what to do when verification fails. Google Pay doesn't need this step — Google verifies the page's TLS handshake on the buyer's first wallet sheet.

Testing. http://localhost shows the card field but not the wallet buttons (Apple Pay refuses non-HTTPS). To test them end-to-end, run your integration on an HTTPS dev domain (any tunnel with a public https:// URL), add that domain under Settings → Wallet Domains in the dashboard and complete the Apple Pay domain verification; Google Pay needs no extra step.


Creating elements​

The card element is your payment surface. Create an elements collection, add the card element, and optionally mount email and save-for-future-use alongside it:

const elements = vora.elements.create({
theme: { font: { size: "16px" }, color: { text: "#1a1a1a" } },
});

const card = elements.create("card", {}); // no style → the SDK default look applies
card.mount("#card-element");

// Optional — separate SDK-rendered fields you place and style yourself:
const email = elements.create("email", {});
email.mount("#email-element");

const save = elements.create("save-for-future-use", {});
save.mount("#save-element");

// Submit collects from every mounted element and drives the charge.
// Branch the result in the order under "SubmitResult shape" below —
// error → chargeStatus → token → charged — and confirm settlement via the
// webhook before fulfilling.
const result = await elements.submit();

The elements collection shares theme defaults across child elements.

SubmitResult shape​

interface SubmitResult {
token?: string; // vp_pmt_* — present only when a reusable method was vaulted
charged?: boolean; // did money move? true = captured; false = attempted, not moved (held / pending / 3DS); absent = no boolean from the server
chargeStatus?: "succeeded" | "pending" | "requires_action";
redirectUrl?: string; // present ONLY when chargeStatus === "requires_action"
recovered?: true; // outcome recovered by polling after an ambiguous failure
paymentIntentId?: string; // vpi_* — reconcilable reference for a charge
transactionId?: string; // vp_tx_* — reconcilable reference on a guest or recovered charge
last4?: string; // display-only
brand?: string; // "visa" | "mastercard" | … (display-only)
expMonth?: number; // 1-12, camelCase, absent when unavailable —
expYear?: number; // see Tokenization § Card expiry on the result
email?: string;
cardholder?: string; // cardholder name (a plain string, not an object)
billingAddress?: BillingAddressValue;
setupForFutureUse?: boolean; // true when the buyer opted to save (vaulted off_session)
error?: VoraMirrorError; // present iff submit failed
}

SubmitResult is one flat interface — every field is optional. Discriminate at runtime in this order; requires_action and pending must be handled before you look at token / charged, because neither is a completed charge and both can fall through a token-first branch:

  1. error — validation or charge failure. Surface result.error.message. Nothing was charged.
  2. chargeStatus: "requires_action" — the charge needs a 3-D Secure challenge and nothing is charged until it completes. Send the buyer to result.redirectUrl (single-use, bearer-equivalent — do not log or persist it).
  3. chargeStatus: "pending" — the charge was submitted and settlement is asynchronous; charged is false. A token may be present when a buyer is on the session (the card is vaulted at authorisation) — it is not a signal that the charge settled. Await the charge.* webhook, which is authoritative; paymentIntentId is your reconcilable reference. Do not start a second payment.
  4. token — a reusable vp_pmt_* was vaulted. On a charge-and-save session the card was already charged in the same step (or, on a captureMethod: "manual" session, authorised and held: charged is false and the money moves only when you capture) — do not also call POST /v1/payment_intents for that session; on a tokenize-only session nothing has been charged and the token is what you charge server-side. Branch on which session you created — Tokenization.
  5. charged — true means money moved: a guest (no buyer on the session) gets charged: true alone, charged once, nothing vaulted, no token; on charge at submit a buyer gets it beside token. false means a charge was attempted and money has not moved; chargeStatus says why (pending, requires_action, or succeeded for a hold). Absent means the SDK had no boolean from the server: treat it as unknown, never as paid or failed. Fulfil only on charged === true. last4 / brand are display-only.

recovered: true marks an outcome resolved by polling after an ambiguous failure the browser never saw a clean response for. The charge is real — never retry a recovered charge; the token, when a buyer was on the session, arrives on the charge.succeeded webhook. Do not act on charged for a recovered result either: it carries no paymentIntentId, only transactionId. Confirm the payment server-side first. The interface above lists the fields you branch on; email, cardholder, billingAddress, setupForFutureUse and picker ride the same object when their element or flow was involved. AVS/CVV results are not in it: read avs_result_code / cvv_result_code on your server from the charge.succeeded / charge.failed webhooks.

if (result.error) {
// failed — nothing was charged
} else if (result.chargeStatus === "requires_action") {
window.location.href = result.redirectUrl; // 3-D Secure; confirm on the webhook
return;
} else if (result.chargeStatus === "pending") {
// charge in flight — await the charge.* webhook. Do NOT charge again.
} else if (result.recovered) {
// resolved by polling after a lost response. Never fulfil on result.charged
// here; confirm server-side (result.transactionId). Do NOT charge again.
} else if (result.token) {
// charged + vaulted (buyer on session)
} else if (result.charged) {
// guest charge-only (no token)
}

The client result is a UX signal; confirm settlement via the webhook before fulfilling. Optional fields are present iff the corresponding element was mounted. setupForFutureUse is a boolean — true when the buyer checked the save-for-future-use box; the token's reusability scope lives server-side (Tokenization).


card — PAN / expiry / CVC iframe​

Your payment surface. The iframe is served directly from the active binder's CDN; your DOM never sees the PAN.

Options​

interface CardElementOptions {
style?: VoraFieldStyle;
classes?: { focus?: string; invalid?: string; complete?: string };
placeholder?: { number?: string; expiry?: string; cvc?: string }; // these three keys ONLY
challengeTimeout?: number; // ms — default 300_000 (5 minutes); clamped to 600_000 (10-min) max
locale?: string; // BCP-47, e.g. "en-US"; English only today
disable3dsModal?: boolean; // no effect where the processor draws its own challenge sheet

// Card-iframe rendering knobs. Each is opt-in (default off);
// the binder ignores a knob for UI it doesn't render.
disableAutofillAssist?: boolean; // suppress the binder's autofill / login-link pill
hidePostalCode?: boolean; // hide the inline postal-code input the card iframe renders
hideBrandIcon?: boolean; // hide the card-brand icon inside the card-number field

// Payment-method display controls. Monolith-embed binders only;
// direct-card binders ignore them.
display?: "all" | "addOnly" | "storedOnly" | "supportsTokenization"; // default "all"
autoSelectOption?: "first" | "firstStored" | "firstNonStored" | "none"; // default "first"
compactPaymentOptions?: boolean; // default true — tighten spacing between method rows
}

Placeholder text — three keys, and only three​

placeholder on the combined card element takes an object, not a string. Exactly three keys are accepted:

const card = collection.create("card", {
placeholder: { number: "Card number", expiry: "MM / YY", cvc: "CVC" },
});

An unrecognised key is not applied — the field keeps its default label and the SDK logs a console.warn naming the accepted keys. The keys are number, expiry, cvc, not the processor's own names (expiryDate, securityCode).

Placeholder text is applied by binders that render their own field labels; a binder whose card iframe doesn't expose per-field placeholders ignores it, the same way it ignores any rendering knob for UI it doesn't draw. On the discrete fields (card-number / card-expiry / card-cvc) placeholder is a plain string on each element instead — see Custom checkout.

Don't confuse this with style.color.placeholder, which sets the placeholder's colour. The two are unrelated and both can appear in the same options object.

Card-iframe rendering knobs​

These three options suppress optional UI the binder renders inside its card iframe. Each is OPT-IN — set to true to suppress, omit (or set to false) to leave the binder's default behavior intact. A knob for UI the binder doesn't render is ignored silently.

OptionWhat it doesTypical use case
disableAutofillAssistHides the saved-card / login-link autofill pill the card iframe renders inline.Merchants whose checkout already has its own saved-card UX, or who want a minimal-chrome card field with no autofill prompt.
hidePostalCodeRemoves the postal-code input the card iframe renders alongside PAN / expiry / CVC.Merchants collecting postal code elsewhere in their own form — avoids asking for it twice.
hideBrandIconHides the card-brand icon (Visa / Mastercard / …) the binder renders inside the card-number field.Merchants whose visual style requires a flat-text card field with no inline brand icon.

Payment-method display controls​

On a monolith-embed binder, the card mount renders the full payment-method list — the new-card form, any saved methods, and the Apple Pay / Google Pay buttons (see Apple Pay & Google Pay). These options shape that list. Direct-card binders render only the card field and ignore them. Saved methods only appear when a returning buyer is identified on the session and that buyer vaulted a card in a prior session — see create-session → buyer identification; a guest or first-time buyer has none, so display: "storedOnly" would render an empty list for them.

OptionValuesDefaultWhat it does
display"all" · "addOnly" · "storedOnly" · "supportsTokenization""all"Which method surfaces render. "all" shows the full list including the wallet buttons; "addOnly" shows only the new-card form; "storedOnly" shows only saved methods; "supportsTokenization" shows only methods that support tokenization. UI-surface only — it does not change what is charged.
autoSelectOption"first" · "firstStored" · "firstNonStored" · "none""first"Auto-selects a method on load so the buyer isn't forced to pick first. Selection is by position — "first" selects whatever the binder lists first (often a wallet when wallets precede the card). Defaults to "first"; pass "none" to leave nothing selected.
compactPaymentOptionstrue · falsetrueTightens the vertical spacing between method rows for a denser layout. Defaults to true; pass false for the spacious layout.

Events​

card.on("ready", () => { /* iframe loaded and accepting input */ });
card.on("change", (event) => {
// event.complete: boolean — all of number/expiry/cvc are valid
// event.error?: { code: string; message: string } // scrubbed validation error; undefined when the field is valid
});
card.on("focus", () => {});
card.on("blur", () => {});

Collect — use the collection's submit()​

The canonical collect path is elements.submit() on the collection — see SubmitResult shape for the unified result and its branch order:

const elements = vora.elements.create();
const card = elements.create("card", {}); // no style → the SDK default look applies
card.mount("#card-element");
const result = await elements.submit(); // collection-level submit charges + (when a buyer is on the session) vaults

There is no card.submit() and a card created via elements.create() has no .tokenize() method — submit() is a collection method (elements.submit()), and it is what charges the card and resolves with the flat, five-outcome SubmitResult.

A vaulted vp_pmt_* is bound to the buyer on the session that minted it; its reusability is governed by setup_for_future_use — Tokenization.


Discrete card fields​

card-number, card-expiry, and card-cvc are the split-field alternative to the combined card element. Instead of one iframe that renders PAN + expiry + CVC together, you mount three independently-placed secure fields — each into its own container — and style and position them anywhere on your page. The PAN, expiry, and CVC still live inside secure field iframes, so your DOM never sees card data.

The three fields share one secure-fields controller and tokenize together via a single collection.submit(), which resolves the same vp_pmt_* token the combined card element produces. You never tokenize a field on its own.

Availability — "elements" mode only

Discrete card fields require a session created with integrationMode: "elements" and a binder that renders discrete secure fields. If the active binder doesn't support them, .mount() throws frame_unsupported_element — fall back to the combined card element or route the buyer to a supported binder. In the default "embed" mode the card is always the one combined box.

The rules​

  • Mount all three. card-number, card-expiry, and card-cvc must all be created and mounted before submit(). Mounting only some can't produce a complete card, so submit() returns frame_field_validation_failed.
  • Don't mix with the combined card. A collection holds one card-entry surface — either the combined card element or the three discrete fields, never both. Mixing them returns frame_field_validation_failed from submit() (they would tokenize twice).
  • One submit() for all three. Because they share a controller, collection.submit() vaults all three fields in a single call — you call it once, not per field.

Options​

Each discrete field takes the same base options; card-number adds showIcon:

interface DiscreteCardFieldOptions {
style?: VoraFieldStyle; // the same token set every element accepts
placeholder?: string; // placeholder shown when the field is empty
disabled?: boolean; // default false — no input accepted when true
}

// card-number only:
interface CardNumberElementOptions extends DiscreteCardFieldOptions {
showIcon?: boolean; // show the card-brand icon inside the field; default false
}
// card-expiry and card-cvc take DiscreteCardFieldOptions as-is.
OptionFieldsDefaultWhat it does
styleall threeSDK default floor, then the collection themeThe unified VoraFieldStyle token set (font + color), merged per property over the SDK default. See Customize.
placeholderall threeSDK default (merged per key) — see Customize → DefaultPlaceholder text shown when the field is empty.
disabledall threefalseDisables input on the field. Honored on the direct-card binder only — the secure-fields binder has no native disabled state and drops it.
showIconcard-number onlyfalseShow the card-brand icon (Visa / Mastercard / …) inside the number field.

Mount and submit​

const collection = vora.elements.create({
theme: { font: { size: "16px" }, color: { text: "#1a1a1a" } },
});

// Create + mount all three, each into its own container:
collection.create("card-number", { placeholder: "Card number", showIcon: true }).mount("#card-number");
collection.create("card-expiry", { placeholder: "MM / YY" }).mount("#card-expiry");
collection.create("card-cvc", { placeholder: "CVC" }).mount("#card-cvc");

// One submit() tokenizes all three fields together. Discrete fields are
// elements mode, where a session can charge at submit — so branch all five
// outcomes (see "SubmitResult shape"):
const result = await collection.submit();
if (result.error) {
// Validation or tokenize failure — surface result.error.message
} else if (result.chargeStatus === "requires_action") {
window.location.href = result.redirectUrl; // 3-D Secure; nothing charged yet
} else if (result.chargeStatus === "pending") {
// charge in flight — await the charge.* webhook. Do NOT charge again.
} else if (result.recovered) {
// resolved by polling after a lost response. Never fulfil on result.charged
// here; confirm server-side (result.transactionId). Do NOT charge again.
} else if (result.token) {
// vp_pmt_* minted from all three fields. Charge it server-side via
// POST /v1/payment_intents — unless the session charged at submit, in which
// case the charge already ran. result.last4 / result.brand are display-only.
} else if (result.charged) {
// guest charge at submit — charged once, nothing vaulted
}

The SubmitResult shape is identical to the combined card element's — see SubmitResult shape. Optional sibling-element values (email, cardholder, billingAddress, setupForFutureUse) are present when those elements were also mounted in the collection.

The discrete fields expose the same on() event API as every element, but event granularity depends on the binder behind your account — field-scoped on the direct-card binder, whole-form on secure-fields, where focus / blur do not fire. Treat change as a form-level signal unless you gate on the binder; see Element events.


email — Email input​

VORA-rendered <input type="email"> with HTML5-aligned validation. Renders as a standalone field on every binder profile.

Options​

interface EmailElementOptions {
style?: VoraFieldStyle;
placeholder?: string; // default "you@example.com"
required?: boolean; // default true
}

Collect​

Returns { email: string } in the submit result. This element renders and validates an email input and hands you the value back — it does not attach the email to the payment or the buyer on its own.

To associate an email with the buyer (so their transactions link together and duplicate-charge lookups work), set buyerEmail when you create the session — see Buyer identification. That is the field that flows to the buyer record.

Use this element when you want VORA to render + validate the email field for you: read result.email from the submit result and pass it as buyerEmail on your server's session-create call (or set buyerEmail directly if you already have the address).


cardholder and address — inputs in your page​

Both are "elements"-mode fields, and where they render depends on the field. cardholder is always a plain <input type="text"> that the SDK creates and appends to your container; it carries autocomplete="cc-name", so the browser may autofill it from a saved card. address is a small form the SDK renders the same way on a connection whose capability map declares it proxy; on a connection that declares it native, the SDK mounts the processor's own address element in that container, and that element renders inside the processor's isolated iframe like the card field. Which one you get is decided per connection by the server, not by anything you pass.

For the fields that sit in your page (the cardholder name always; the address form on a proxy connection), any other script on the page can read them: a tag manager, an analytics vendor, a compromised dependency. Card number, expiry and CVC never leave the iframe and your SAQ-A scope is unchanged, but the buyer's name, and on those connections the billing address, are only as private as your page-script hygiene. If you cannot vouch for every script on the checkout page, use embed mode, where the card mount renders these fields inside the iframe. The security reference states the same rule, and the capability matrix says which rendering your connection declares.

Options​

interface CardholderElementOptions {
style?: VoraFieldStyle;
placeholder?: string; // default "Name on card"
required?: boolean; // default true
}

interface AddressElementOptions {
style?: VoraFieldStyle;
mode: "billing"; // the only mode today
fields?: {
line1?: { required?: boolean };
line2?: { hidden?: boolean }; // hide the apartment / suite line
city?: { required?: boolean };
state?: { required?: boolean };
postalCode?: { required?: boolean };
country?: { default?: string }; // ISO 3166-1 alpha-2, default "US"
};
allowedCountries?: string[]; // ISO 3166-1 alpha-2; omit for all
}

Mount and collect​

const collection = vora.elements.create();
const card = collection.create("card");
const cardholder = collection.create("cardholder", { placeholder: "Name on card" });
const address = collection.create("address", { mode: "billing", fields: { line2: { hidden: true } } });

card.mount("#card");
cardholder.mount("#cardholder");
address.mount("#billing-address");

const result = await collection.submit();
// result.cardholder → "Ada Lovelace"
// result.billingAddress → { line1, line2?, city, state, postalCode, country }

submit() forwards both values with the card token; you do not send them separately. The billing address is what address verification runs against, so read Address verification for which fields to collect per country.


save-for-future-use — Opt-in checkbox​

VORA-rendered checkbox + label that controls the reusability of the token your submit returns; it is always SDK-rendered, on every binder, and has no scope option. Checked, the submit vaults the token as reusable (setup_for_future_use: off_session); unchecked, single-use (null) — usable only by the originating intent. A server-side caller can set the scope without the checkbox via POST /v1/tokens. Full model: Tokenization — reusability.

Compliance lock​

The checkbox starts unchecked and there is no defaultChecked option: storing a payment method for future merchant-initiated charges needs the buyer's active opt-in, and a pre-checked box would void that consent record.

Options​

interface SaveForFutureUseElementOptions {
style?: VoraFieldStyle;
label?: string; // default "Save this card for future purchases"
}

Collect​

Returns { setupForFutureUse: boolean } — true when the buyer checked the box.


payment-method-picker — Method-selector button row​

A VORA-rendered button row that lets the buyer pick which payment method to use. Unlike the other elements, the picker does not tokenize or charge — it surfaces the buyer's choice on submit() and leaves your checkout UX to route the next step (mount the card element, hand off to express-checkout, and so on).

Available on direct-card (proxy) binders. On a monolith-embed binder the active binder renders its own method selector, so elements.create('payment-method-picker', …) throws frame_unsupported_element at create time — drop the element and let the binder's UI handle method selection.

Options​

create() takes a required options object — elements.create('payment-method-picker', { … }).

interface PaymentMethodPickerElementOptions {
style?: VoraFieldStyle;
methods?: PaymentMethodType[]; // omitted/empty = every method the binder supports
defaultMethod?: PaymentMethodType; // pre-select on render (still mutable); default: no pre-selection
buttonLayout?: "horizontal" | "vertical"; // default "horizontal"
}

type PaymentMethodType =
| "card"
| "apple_pay"
| "google_pay"
| "link"
| "cashapp"
| "klarna"
| "affirm";
OptionValuesDefaultWhat it does
methodsPaymentMethodType[]every supported methodMethods to surface, in render order (left-to-right for horizontal, top-to-bottom for vertical). The list is capability-filtered against the active session's binder — see below.
defaultMethoda PaymentMethodTypenonePre-selects a method on render so the buyer isn't forced to pick first. The selection is still mutable. A defaultMethod that isn't a recognized PaymentMethodType throws frame_field_validation_failed at create time.
buttonLayout"horizontal" · "vertical""horizontal"Row of buttons (wraps on narrow viewports) vs. one button per row.

Capability filtering​

The methods you pass are intersected with the session's binder capability map: any method the binder declares not_available for is hidden even if you listed it, and methods you didn't list are hidden too. When you explicitly supply methods and one or more are dropped, the SDK emits a one-time console.warn naming the filtered methods so the filtering isn't silent. If the intersection is empty, mount() throws frame_unsupported_element — review your methods array or route the buyer to a binder that supports at least one of them.

Collect — the nested picker result​

The picker contributes a nested picker group to the collection's SubmitResult, present only when the element was mounted. The picker neither charges nor vaults, so it never sets token / charged:

interface PickerSubmitResult {
selectedMethod: PaymentMethodType; // the method the buyer chose
}
const elements = vora.elements.create();
const picker = elements.create("payment-method-picker", {
methods: ["card", "apple_pay", "google_pay"],
defaultMethod: "card",
buttonLayout: "horizontal",
});
picker.mount("#picker-element");

const result = await elements.submit();
if (result.error === undefined) {
switch (result.picker?.selectedMethod) {
case "card":
// mount the `card` element and run the card path
break;
case "apple_pay":
case "google_pay":
// hand off to `express-checkout`
break;
// "link" | "cashapp" | "klarna" | "affirm" — route per your checkout UX
}
}

Read result.picker?.selectedMethod to branch; the optional chaining covers the picker not being mounted.


Styling & theming​

Every element accepts a style option — the VoraFieldStyle token set (font, color, spacing, border, background, per-layer surfaces) — plus a classes option for focus / invalid / complete pseudo-states. The complete styling reference — the full token set, format-validation rules, the inner-vs-outer-iframe model, the three reference themes, and an interactive playground — lives on one page:

➡️ Customize the look & feel


What's next​