Skip to main content

Customize the look & feel

Make the card field match your brand — fonts, colors, radius, spacing — with the style option (VoraFieldStyle), in both render modes. The mode a session renders in ("embed" by default, "elements" where your account supports it — Render modes) selects the renderer, and the same token lands differently per renderer: the token map below says where. In "embed" the card mount is your whole payment surface (card field, wallet buttons, billing address, method picker), so tokens reach more surfaces there; in "elements" you place those pieces yourself. Read the integrationMode echoed on the session to know which surface rendered, then confirm with the live iframe.

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.

Loading interactive playground...

What the tool gives you:

  1. Theme & JSON tab — pick a preset (Default / Light / Dark) or hand-edit the VoraFieldStyle JSON.
  2. Match my site tab ⭐ — paste your site's public URL or stylesheet, and we extract a VoraFieldStyle config that matches your fonts, colors, radius, and spacing.
  3. 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.
  4. Preview (live sandbox) — click Try with live SDK to mount the real card iframe against a sandbox demo merchant.

The preview is the real iframe, not a mock — the field is rendered by the session's active surface, so only the live iframe is faithful. The style JSON and the snippet are the contract.


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 and renders identically on both surfaces. The themes below therefore put in style only the tokens that reach the iframe on both surfaces — font.family, color.text / placeholder / error / focus — and border, padding, background, font-size / font-weight on the wrapper. A surface-specific token placed in style for the wrong surface is not applied, and the SDK logs a console.warn naming the dropped token.

Where each token lands​

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.familyCard iframeCard iframe
font.size, font.weightYour wrapper <div>Card iframe
color.text / .placeholder / .error / .focusCard iframeCard iframe
color.label / .accentCard iframeYour wrapper <div>
background, surface.fieldCard iframeCard iframe
surface.container / .page / .hover / .uncheckedCard iframeYour wrapper <div>
spacing.paddingYour wrapper <div>Card iframe
spacing.marginYour wrapper <div>Your wrapper <div>
border.radius / .color / .widthCard 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.

Accounts on a direct-card processor render a third way: the card iframe takes font.* and color.text / .placeholder / .error only; border, spacing, background, surface.* and color.focus are dropped (the console names them) and belong on your wrapper.

Default — what you get if you pass nothing​

const elements = vora.elements.create();
const card = elements.create("card", {
// No style — the SDK's default look applies, property by property.
});

A card field you do not style is still a visible field. Any property you leave out is filled from this default, so an empty style gives you all of it:

GroupDefault
fontsystem font stack, 16px, weight 400
colortext #1a1a1a · placeholder #6b7280 · error #b91c1c
border1px #d1d5db, radius 6px
spacingpadding 12px
background#ffffff
placeholder text1234 1234 1234 1234 · MM / YY · CVC

Three things to know about it:

  • It is a floor, not an override. Layers merge in the order default → collection theme → element style, and inside font, color, spacing and border they merge per property. Passing { color: { text: "#fff" } } changes the text colour and keeps the default border, placeholder colour and everything else; it does not drop you back to an unstyled field. background and surface are replaced whole.
  • It applies to the card surfaces only — card, card-number, card-expiry and card-cvc, the ones rendered inside the secure iframe. email, cardholder and address are ordinary inputs in your own page and inherit your stylesheet as before.
  • font.size is 16px on purpose. Mobile Safari zooms the whole page when a focused input is smaller than that, which throws the layout around under the buyer's thumb mid-payment. If you set your own size, keep it at 16px or above on mobile.

How much of the default you see follows the token map exactly as your own values do: a default border lands only where border.* reaches the card iframe. On the "elements" surface it does not, and what keeps an unstyled field visible there is the placeholder text, background and padding.

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; where it doesn't, the token is dropped and the SDK logs a console.warn naming it once per surface — 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.accent colors interactive / selected affordances (the selected payment-method indicator).
  • color.label colors field labels; leave it unset to follow color.text.
  • color.focus colors the focus ring on the active input.
  • surface.* sets per-layer backgrounds — page, container, unchecked, hover, field. Any surface.* value wins over the flat background shorthand.

The container's own classes​

Every mounted element stamps classes on your container — the element you passed to mount() — so your stylesheet can react to what happens inside the secure iframe:

ClassWhen
vora-fieldalways, from mount until unmount
vora-field--{type}always — vora-field--card, vora-field--card-number, vora-field--email, …
vora-field--focuswhile the field is focused
vora-field--invalidwhile the field reports a validation error
vora-field--completewhile the field validates

There is deliberately no --empty — the change event carries complete and an optional error, and no emptiness signal. Nothing is written to your element's inline style. The classes are automatic and separate from the classes option below, which lets you name your own for the same three states; both can be used together.

.vora-field {
box-sizing: border-box;
min-height: 44px;
padding: 12px;
border: 1px solid #d1d5db;
border-radius: 6px;
background: #fff;
transition: border-color 0.15s, box-shadow 0.15s;
}
.vora-field--focus { border-color: #2563eb; box-shadow: 0 0 0 3px rgb(37 99 235 / 0.15); }
.vora-field--invalid { border-color: #b91c1c; }
.vora-field--complete { border-color: #059669; }

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, 600 doesn'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):

OptionValuesDefaultEffect
compactPaymentOptionstrue · falsetrueThe 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.
hidePostalCodetrue · falsefalseHide the postal-code input the card iframe renders.
hideBrandIcontrue · falsefalseHide the card-brand icon inside the card-number field.
disableAutofillAssisttrue · falsefalseHide the binder's autofill / login-link pill.
placeholder{ number?, expiry?, cvc? }SDK default, merged per key (Default)Custom placeholder text per sub-field.

Method picker & wallet buttons:

OptionValuesDefaultEffect
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 the style option 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​

SurfaceControllable viaWhat 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 iframeYour CSS on the mount <div> and its parentsAnything — 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 optionAlmost anything — these render in your DOM, not an iframe
Non-Vora content (shipping, summary, header, footer, CTA, layout)Your HTML + CSSAnything — the SDK doesn't touch it

Things you can't do, and why​

Want to doWhy it doesn't workWhat does work
box-shadow, gradients, transitions on the iframeAllowlist 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 onlySingle value to the iframe + outer-wrapper padding for asymmetric spacing
Recolor the wallet / card iconsVendor-supplied artwork rendered by the binderThe style tokens recolor the surfaces around the marks
Custom fonts (Poppins, Inter) inside the iframeThe iframe is served from the binder origin, which doesn't fetch your fontsThe iframe falls back to a system sans; your outer-DOM labels/fields use custom fonts freely
Float a label inside the iframeIframe content is secure-fields-rendered — can't inject DOMIn 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​

Point your integration at a test key and swap the style object between the themes above. A sandbox test card such as 4111 1111 1110 1203 runs the full successful tokenization path at an ordinary total, so you can see the completed state; Test mode has the totals that produce a decline if you want to style those states too.


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 is collected either as the combined box (number + expiry + CVC together) or as the three discrete secure fields — your choice per integration
  • Elements reference — per-element options + collect shapes
  • 3D Secure — sending the buyer to the bank's challenge and back