Customize the look & feel
Make the card field match your brand — fonts, colors, radius, spacing. You style it with the same style API (VoraFieldStyle) in both render modes of Embedded Fields — embed and elements (custom layout). One API — but it lands differently per surface:
A session renders in one of two modes — "embed" (default) or "elements" — chosen per session via integrationMode (see Render modes: embed vs elements for the model; how to set it →). That choice selects the renderer, and the same VoraFieldStyle lands differently per renderer:
- The
"embed"(default) surface appliesfont.family, allcolor.*,background/surface.*, andborder(border color, plusborder.radiusandborder.widthmapped to the binder's preset scale — radius →none/subtle/rounded, width →none/thin/thick) inside the card iframe. It does not applyfont.size,font.weight,spacing.padding, orspacing.marginto the iframe — style those on your own outer wrapper<div>(see inner vs outer iframe). - The
"elements"(discrete) surface appliesfont.family/font.size/font.weight,color.text/placeholder/error/focus,background(orsurface.field), andspacing.paddingto the single card-field iframe. It does not applyborder.*,spacing.margin,color.label/accent, or the container-levelsurface.*layers — style those on the wrapping element in your own DOM. (Theborderpreset-scale mapping is the"embed"surface's behavior, not elements.)
It's the same token set, validated the same way — but what each token actually does depends on the surface the session rendered. You request "elements" per session; where your account doesn't support it the session falls back to "embed" (it never errors). Read the integrationMode echoed back on the session and the capability map from vora.sessions.retrieve() to confirm what actually rendered, then verify with the live render.
This is the one home for everything visual: the interactive playground, reference themes, the complete VoraFieldStyle token set, and the inner-vs-outer-iframe model. (The Elements reference covers per-element options; styling lives here.)
What's in the card mount. In
"embed"(default) the card mount is your whole payment surface — card field, wallet buttons, billing address, and method picker together; onlysave-for-future-useare your own DOM. In"elements"you place those pieces separately. That's why the samestyletoken lands on more surfaces in embed than in elements. (How the modes work →.)
Interactive playground
Edit the VoraFieldStyle JSON by hand, pick a preset, or paste your site URL and we'll match it for you — then copy the generated integration snippet.
What the tool gives you:
- Theme & JSON tab — pick a preset (Default / Light / Dark) or hand-edit the
VoraFieldStyleJSON. - Match my site tab ⭐ — paste your site's public URL or stylesheet, and we extract a
VoraFieldStyleconfig that matches your fonts, colors, radius, and spacing. - Code generator — a copy-paste snippet that reflects your JSON. Replace
vp_pk_test_...with your own publishable key and wire the session-mint step to your server. - Preview (live sandbox) — click Try with live SDK to mount the real card iframe against a sandbox demo merchant.
This tool does not fake-render the card field. The only visual preview is the real iframe via Try with live SDK, because the field is rendered by the session's active surface (which applies the style tokens its own way) — a faithful preview can only come from the live iframe. If the live sandbox isn't enabled in this environment (an endpoint returns 503), the JSON editor + generated snippet still work; run the sample app to see it rendered. The style JSON and the snippet are the exact contract; trust those, and confirm the look with the live iframe or the sample app.
Three reference themes
The style option is an allowlist-validated object — anything outside the allowlist throws frame_style_invalid before the iframe mounts, so your CI catches misuse before merchants see it.
Two places, and the split is portable. The style object reaches inside the secure card iframe; your own CSS on the mount `<div>` styles the box around it. Which style tokens actually reach the iframe depends on the render surface (see inner vs outer iframe) — but your wrapper CSS renders identically on both "embed" and "elements". So the themes below use the copy-paste-anywhere split: font.family, color.text / placeholder / error / focus, and background go in style (these reach the iframe on both surfaces), and border, padding, and any font-size / font-weight go on your wrapper `<div>`. Written this way, the snippet is correct whichever way a session renders — nothing silently drops.
The themes above use that portable split; this table is the full map. If you know the session's surface you can move a surface-specific token into style where it lands there — but put it there for the wrong surface and it's silently ignored, which is why the wrapper split is the safe default. A Your wrapper <div> cell means the binder does not apply that token to the iframe on that surface — style it in your own DOM.
| Style token | "embed" (default) | "elements" (discrete) |
|---|---|---|
font.family | Card iframe | Card iframe |
font.size, font.weight | Your wrapper <div> | Card iframe |
color.text / .placeholder / .error / .focus | Card iframe | Card iframe |
color.label / .accent | Card iframe | Your wrapper <div> |
background, surface.field | Card iframe | Card iframe |
surface.container / .page / .hover / .unchecked | Card iframe | Your wrapper <div> |
spacing.padding | Your wrapper <div> | Card iframe |
spacing.margin | Your wrapper <div> | Your wrapper <div> |
border.radius / .color / .width | Card iframe ¹ | Your wrapper <div> |
¹ On "embed", border.radius and border.width snap to the binder's preset scale — radius → none / subtle / rounded, width → none / thin / thick — and border.color applies as a direct color; a non-px value is reported unsupported and belongs on the wrapper. See inner vs outer iframe.
Default (inherits your site)
const elements = vora.elements.create();
const card = elements.create("card", {
// No style override — inherits browser defaults.
});
Light + branded accent
// style: the tokens that reach the iframe on BOTH surfaces.
const card = elements.create("card", {
style: {
font: { family: "system-ui, -apple-system, sans-serif" },
color: { text: "#1a1a1a", placeholder: "#9ca3af", error: "#dc2626" },
background: "#ffffff",
},
});
/* Your own CSS on the element you mount into — border, radius, padding, and
font size/weight. Renders the same on "embed" and "elements". */
#card-mount {
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 12px;
font-size: 16px;
font-weight: 400;
}
Dark mode
const card = elements.create("card", {
style: {
font: { family: '"Inter", system-ui, sans-serif' },
color: { text: "#e5e7eb", placeholder: "#6b7280", error: "#f87171" },
background: "#1f2937",
},
});
#card-mount {
border: 1px solid #374151;
border-radius: 12px;
padding: 14px;
font-size: 16px;
font-weight: 500;
}
The VoraFieldStyle token set
All elements accept a style option whose shape is governed by an allowlist (any key not in the allowlist throws frame_style_invalid at validation time, before the iframe mounts).
interface VoraFieldStyle {
font?: {
family?: string; // alphanumeric + space + comma + double-quote + hyphen only
size?: string; // single dimension: "16px" / "1rem" / "100%"
weight?: "400" | "500" | "600" | "700"; // exact string, not a free-form number
};
color?: {
text?: string; // hex / rgb() / rgba() / hsl() / "black" | "white" | "red" | "green" | "blue" | "yellow" | "gray" | "transparent" | "currentcolor"
placeholder?: string; // same format set
error?: string; // same format set
label?: string; // field-label color; defaults to follow color.text when unset
accent?: string; // accent / selected payment-method indicator color
focus?: string; // focus-ring color
};
spacing?: {
padding?: string; // SINGLE dimension only — "12px", NOT "12px 16px 8px 16px"
margin?: string; // same single-dimension constraint
};
border?: {
color?: string; // same color format set
width?: string; // single dimension
radius?: string; // single dimension
};
background?: string; // TOP-LEVEL string (not background.color); flat fill for the field + container
surface?: { // per-layer background colors; any value set here wins over `background`
field?: string; // the card input field background
container?: string; // a method-option row / container background
page?: string; // the outer page background behind the option rows
hover?: string; // a method-option row background on hover
unchecked?: string; // an unselected method-option row background
};
}
A token is applied where the binder exposes a surface for it and ignored silently where it doesn't — a neutral-by-design contract, so the payload stays portable across binder profiles. The payment-method icons (the card glyph, the Apple Pay / Google Pay marks) are vendor-supplied artwork rendered by the binder and are not themeable; the tokens recolor the surfaces around them.
Theming the method list & surfaces
On a combined card mount the style payload themes the whole payment surface, not just the card field:
color.accentcolors interactive / selected affordances (the selected payment-method indicator).color.labelcolors field labels; leave it unset to followcolor.text.color.focuscolors the focus ring on the active input.surface.*sets per-layer backgrounds —page,container,unchecked,hover,field. Anysurface.*value wins over the flatbackgroundshorthand.
Pseudo-selectors — the classes option
For focus / invalid / complete states (which style can't express), attach your own CSS classes:
const card = elements.create("card", {
classes: {
focus: "my-card-focus", // applied while focused
invalid: "my-card-invalid", // applied when validation fails
complete: "my-card-complete", // applied when all fields valid
},
});
.my-card-focus { outline: 2px solid #3b82f6; }
.my-card-invalid { outline: 2px solid #dc2626; }
.my-card-complete { border-color: #10b981; }
Format validation — what gets rejected
Even within an allowed key, values are regex-validated to prevent CSS-injection. These throw frame_style_invalid:
color: { text: "var(--my-color)" }— CSS custom properties (would let a page exfiltrate state via computed-style probing).font: { family: "'; body { display: none; }" }— quote injection.spacing: { padding: "12px 16px" }— shorthand; only a single dimension is allowed.font: { weight: 600 }— weight is a strict string union;"600"works,600doesn't.border: { width: "thick" }— keywords; only\d+(\.\d+)?(px|em|rem|%).
For asymmetric padding, shadows, transitions, or hover states, apply those on your outer wrapper <div> (your DOM) — visually identical, no allowlist limit. Theme set on the collection is inherited by all child elements; element-level style overrides per-property (shallow top-level merge).
Layout & display options
Beyond the style token set, a few per-element options control layout and which UI the binder renders — the look-and-feel knobs that aren't colors or fonts. They live on each element's options object (full per-element context in the Elements reference); they're gathered here so you can tune the whole look from one page. All are UI-surface only — they change what the buyer sees, never what's charged.
Card field (card element):
| Option | Values | Default | Effect |
|---|---|---|---|
compactPaymentOptions | true · false | true | The compact-vs-spacious density toggle — true tightens vertical spacing between payment-method rows; false gives a roomier layout. |
display | "all" · "addOnly" · "storedOnly" · "supportsTokenization" | "all" | Which method surfaces render (new-card form, saved methods, wallets). Monolith-embed binders only. |
autoSelectOption | "first" · "firstStored" · "firstNonStored" · "none" | "first" | Which method is pre-selected on load. |
hidePostalCode | true · false | false | Hide the postal-code input the card iframe renders. |
hideBrandIcon | true · false | false | Hide the card-brand icon inside the card-number field. |
disableAutofillAssist | true · false | false | Hide the binder's autofill / login-link pill. |
placeholder | { number?, expiry?, cvc? } | binder default | Custom placeholder text per sub-field. |
Method picker & wallet buttons:
| Option | Values | Default | Effect |
|---|---|---|---|
buttonLayout | "horizontal" · "vertical" | "horizontal" | Button-row layout on the payment-method-picker and express-checkout elements. |
For per-element specifics (which binder honors which knob), see the Elements reference.
Inner iframe vs outer iframe
The single most important styling concept: what's inside the binder's iframe vs what's your own DOM.
- Inner iframe = what the secure iframe renders. Under
"embed"(default) this is the whole monolith — card number / expiry / CVC plus the wallets, billing address, and method-picker rows. Under"elements"it's the combined card field (number + expiry + CVC). Either way it's served from the secure-fields domain, so you can't reach it with CSS (cross-origin); the only way to style it is thestyleoption above. - Outer iframe = your DOM. The container
<div>you mount into, the label above it, the wrapper around it, every non-PCI field (email, name, summary, CTA — plus billing address when you're in"elements"mode and mount it yourself), the page chrome — all your HTML and CSS, no restrictions.
The rule the SDK enforces: the element is the unit of placement; internal layout is the binder's territory. You decide where each element appears in your page; you can't decide where individual wallet buttons sit inside the card mount.
What you control where
| Surface | Controllable via | What you can change |
|---|---|---|
| Inner iframe (card fields, wallets) | style option on elements.create(...) | Colors (text / label / placeholder / error / accent / focus), per-layer surface backgrounds, border, and font.family. font.size / font.weight / spacing.padding apply only on the "elements" surface; spacing.margin is never applied to the iframe — put those on the outer wrapper |
| Outer wrapper around the iframe | Your CSS on the mount <div> and its parents | Anything — box-shadow, gradients, hover states, transitions, position, size, focus rings, floating labels |
Non-PCI fields (email, save-for-future-use) | Your CSS on the element's mount + the style option | Almost anything — these render in your DOM, not an iframe |
| Non-Vora content (shipping, summary, header, footer, CTA, layout) | Your HTML + CSS | Anything — the SDK doesn't touch it |
Things you can't do, and why
| Want to do | Why it doesn't work | What does work |
|---|---|---|
box-shadow, gradients, transitions on the iframe | Allowlist blocks free-form CSS in the iframe (clickjacking defense) | Apply on the outer wrapper <div> — visually identical, fully your CSS |
Multi-value padding ("12px 16px") | spacing.padding is single-dimension only | Single value to the iframe + outer-wrapper padding for asymmetric spacing |
| Recolor the wallet / card icons | Vendor-supplied artwork rendered by the binder | The style tokens recolor the surfaces around the marks |
Custom fonts (Poppins, Inter) inside the iframe | The iframe is served from the binder origin, which doesn't fetch your fonts | The iframe falls back to a system sans; your outer-DOM labels/fields use custom fonts freely |
| Float a label inside the iframe | Iframe content is secure-fields-rendered — can't inject DOM | In elements mode (see Render modes) the card iframe shows only the input values (no internal labels), so a floating-label wrapper works: draw the box + label on your own outer <div> and mount the card into a padded area inside it |
Test the themes locally
The fastest way to see all three themes against a real sandbox processor is the sample app:
git clone https://github.com/Von-Payments/vonpay.git
cd vonpay/samples/frame-react
cp .env.example .env # fill in vp_sk_test_* + VITE_VONPAY_PUBLISHABLE_KEY
pnpm install
pnpm dev
Open http://localhost:5173, click Create checkout session, then edit the style prop in src/App.tsx to swap between the themes. Vite hot-reloads each change. Test card 4242 4242 4242 4242 (any future expiry / any CVC) renders the full successful tokenization path.
What's next
- Quickstart — end-to-end card-only happy path
- Custom checkout with Elements — mount discrete elements (card, email, name, address) where you choose on the page; the card itself stays one combined field (number, expiry, and CVC together)
- Elements reference — per-element options + collect shapes
- React —
<VoraProvider>+useVora()patterns - 3D Secure — modal harmonization +
confirmPaymentIntent