Skip to main content

Customize the look & feel

Make the card field match your brand — fonts, colors, radius, spacing. You style it with the same style API (VoraFieldStyle) on both Embedded Fields and Elements (custom). One API — but it lands differently per surface:

How style lands depends on your account's binder — the SDK normalizes one VoraFieldStyle onto whichever binder backs your account:

  • The current Embedded Fields binder applies the font + color tokens inside the card iframe, but border, padding, and margin are not applied to the iframe — style those on your own outer wrapper <div> (see inner vs outer iframe).
  • The discrete-field binder behind Elements applies more of the token set — border.width / border.radius map to that binder's preset scale (e.g. none / subtle / rounded), or pass through as field CSS.

It's the same token set, validated the same way — but what each token actually does is binder-specific. vora.sessions.retrieve() reports your account's capability map; confirm the exact result 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.)


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

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 your account's binder (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.

Default (inherits your site)

const elements = vora.elements.create();
const card = elements.create("card", {
// No style override — inherits browser defaults.
});

Light + branded accent

const card = elements.create("card", {
style: {
font: { family: "system-ui, -apple-system, sans-serif", size: "16px", weight: "400" },
color: { text: "#1a1a1a", placeholder: "#9ca3af", error: "#dc2626" },
border: { color: "#e5e7eb", width: "1px", radius: "8px" },
background: "#ffffff",
spacing: { padding: "12px" },
},
});

Dark mode

const card = elements.create("card", {
style: {
font: { family: '"Inter", system-ui, sans-serif', size: "16px", weight: "500" },
color: { text: "#e5e7eb", placeholder: "#6b7280", error: "#f87171" },
border: { color: "#374151", width: "1px", radius: "12px" },
background: "#1f2937",
spacing: { padding: "14px" },
},
});

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.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.

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).


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 binder renders (card number, expiry, CVC — the PCI-restricted inputs). It's served from the binder's 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, shipping, billing, summary, CTA), 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(...)Text / label / placeholder / error / accent / focus colors, per-layer surface backgrounds, border, font, padding, margin
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 binder-rendered — can't inject DOMOn the direct-card binder profile a floating-label wrapper works; see Advanced — binder profiles

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