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, andmarginare 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.radiusmap 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.
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 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.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).
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
styleoption 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
| Surface | Controllable via | What 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 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 binder-rendered — can't inject DOM | On 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
- Quickstart — end-to-end card-only happy path
- Custom checkout with Elements — discrete card fields you lay out yourself
- Elements reference — per-element options + collect shapes
- React —
<VoraProvider>+useVora()patterns - 3D Secure — modal harmonization +
confirmPaymentIntent