Appearance
Presets, colours, design tokens and the rules the SDK silently drops.
One appearance object styles everything: the hosted checkout, the overlay, and every payment
field component. It has three layers, and they apply in this order.
- A
themepreset, if you name one. - The flat keys,
primaryColorand its neighbours. variablesandrules, for token level and selector level control.
Each layer wins over the one above it, so a preset is a starting point rather than a constraint.
From Wajub.js 1.8.0, inherit can put your own page's look under all three: see
Taking your page's look.
await mount('#checkout', {
sessionId,
appearance: {
theme: 'stripe',
primaryColor: '#6366f1',
colorScheme: 'auto',
labels: 'floating',
variables: { colorText: '#0f172a', fontSizeBase: '16px' },
rules: { '.Input': { borderRadius: '8px' } },
},
});Anything the SDK does not recognise is dropped without a word
The object is filtered before it leaves the browser. An unknown key, a selector outside the short list below, a property outside the whitelist, all disappear silently. Nothing throws, and nothing appears in the console, so a typo simply has no effect.
Presets
theme layers a set of flat keys under yours. These are the exact values.
| Preset | What it sets |
|---|---|
stripe | primaryColor: #0570de, borderRadius: 4px, the system font stack, shadow: low |
night | colorScheme: dark, primaryColor: #7c3aed, backgroundColor: #0f172a, buttonTextColor: #f8fafc, inputBackgroundColor: #1e293b, inputBorderColor: #334155, textMutedColor: #94a3b8, borderRadius: 8px, shadow: none |
flat | shadow: none, borderRadius: 0 |
none | Nothing. Only your own keys apply |
stripe is the default look of a Wajub checkout, so naming it changes nothing unless you also
override part of it.
light, dark, system and auto are not presets. From Wajub.js 1.7.1, theme set to one of
them is read as colorScheme, system meaning auto, when you give no colorScheme. Earlier
versions dropped it.
Flat keys
The shorthand layer. Every value is a plain CSS string.
| Key | Type | What it paints |
|---|---|---|
primaryColor | CSS color | The accent, primary buttons, links |
secondaryColor | CSS color | Secondary accents |
backgroundColor | CSS color | The surface behind the form |
buttonTextColor | CSS color | Text on primary buttons |
inputBackgroundColor | CSS color | The fields' own background. The page around them keeps its own |
inputBorderColor | CSS color | The fields' own border. The page's other borders keep theirs |
textMutedColor | CSS color | Helper and secondary text |
successColor | CSS color | Success states and badges |
errorColor | CSS color | Errors and validation messages |
fontFamily | CSS font stack | The whole checkout |
borderRadius | CSS length | Default corner radius |
shadow | low or none | Elevation |
colorScheme | light, dark, auto | light or dark wins over the payer's device and the choice they saved on a hosted page. auto follows the device |
labels | above or floating | Where field labels sit |
disableAnimations | boolean | Removes transitions and motion |
The deprecated theme object
theme: { primaryColor, fontFamily, borderRadius } on the mount config is the old shape. The
SDK still fills it from appearance for backward compatibility, so you never set it yourself.
Note that appearance.theme is a different thing, a preset name.
Variables
The token layer. Each key sets one design token, so it reaches every component that uses that token rather than one surface.
| Key | What it sets |
|---|---|
colorPrimary | The accent colour |
colorBackground | The surface behind the form |
colorText | Main text, in the fields too |
colorTextSecondary | Secondary text, the field labels included |
colorTextPlaceholder | Placeholder text |
colorDanger | Error text |
colorSuccess | Success text |
fontFamily | The font stack. Its first family is loaded when it is a Google Font the checkout knows, such as Poppins or DM Sans |
fontSizeBase | The size of the text typed in the fields, at least 16px on a phone or a tablet. Labels and hints are a step smaller |
fontWeightNormal | Regular text weight |
fontWeightMedium | Medium text weight |
fontWeightBold | Bold text weight |
spacingUnit | The base spacing unit |
borderRadius | Corner radius |
spacingGridRow | The gap between rows of fields |
spacingGridColumn | The gap between columns of fields |
Sixteen keys, and nothing else. A seventeenth is dropped on the way out.
appearance: {
variables: {
fontFamily: 'Inter, system-ui, sans-serif',
fontSizeBase: '15px',
fontWeightMedium: '600',
spacingUnit: '4px',
},
}When a flat key and its token twin are both set, the flat key wins. Pick one and stay on it.
From the checkout shipped with Wajub.js 1.7.1, whatever the version on your page, fontFamily,
fontSizeBase, colorText, colorTextSecondary, colorTextPlaceholder and borderRadius reach
the fields themselves. inputBackgroundColor and inputBorderColor style the fields alone, where
they used to restyle every background and every border, and a colorScheme you give wins over the
payer's device, in a payment field and in the embedded checkout.
From the checkout shipped with Wajub.js 1.7.2, whatever the version on your page:
- The
colorSchemeyou give applies from the first paint on every page, the embedded checkout and the pagecheckout()sends the payer to included, as it did in a payment field. When you give one,autoincluded, the checkout hides its light and dark button, which your scheme undid at once. - The page
checkout()sends the payer to takes your appearance from its first render. It dropped it, and got it back only on a later render. - A
fontFamilythe checkout loads reaches the Stripe card fields too, which fell back to the system font. - Without
fontSizeBase, the fields are 16px on a phone or a tablet, the Stripe card fields included. They were 15px, 13px for a coupon, and iOS zoomed in on focus.
Rules
The selector layer, and the narrowest one. It exists to reshape the form fields, not to restyle the checkout.
Rules reach the fields from 1.7.1
From the checkout shipped with Wajub.js 1.7.1, whatever the version on your page, .Input and
.Label rules style the fields. Before it, rules had no visible effect.
Selectors. Only .Input and .Label, each with at most one of :hover, :focus,
:focus-within, :disabled, ::placeholder, ::selection. .Input is a field's box, its
border, background and corners, focused while its input is and disabled with it. .Label is a
field's label. From the checkout shipped with Wajub.js 1.7.2, whatever the version on your page,
the color and the font of a .Input rule reach the text typed in the field, in the card fields
the checkout draws itself too, and the color of a .Label rule reaches the card holder's label
with Adyen too.
appearance: {
rules: {
'.Input': { borderRadius: '8px', borderColor: '#cbd5e1', padding: '12px' },
'.Input:focus': { borderColor: '#6366f1', outline: '2px solid #c7d2fe' },
'.Input::placeholder': { color: '#94a3b8' },
'.Label': { fontWeight: '500', fontSize: '14px', letterSpacing: '0.01em' },
},
}Properties. Thirty one, in camelCase.
| Group | Allowed |
|---|---|
| Colour | color, backgroundColor, opacity |
| Border | border, borderColor, borderWidth, borderStyle, borderRadius |
| Box | padding and its four sides, margin and its four sides |
| Type | fontFamily, fontSize, fontWeight, fontStyle, lineHeight, letterSpacing, textAlign, textDecoration |
| Effect | boxShadow, outline, outlineColor, outlineWidth, outlineOffset |
Values. Non empty strings only. A value that could load something (url(, image-set(,
@import), run a script (expression(, javascript:) or break out of its declaration (;, {,
}, <, >, \) is refused, which rules out background images and font imports. From the
checkout shipped with Wajub.js 1.7.1, the checkout refuses these values itself, and values over
256 characters, on every path: flat keys, variables and rules, at mount and through
update({ appearance }), whatever the version on your page. From Wajub.js 1.7.2, the SDK drops the
same rules values before they leave your page.
A rule that does not fit is not an error
.Tab, .AccordionHeader, display, position and a background image all pass through
appearance without complaint and without effect. If a rule does nothing, check it against the
three lists above before checking anything else.
Taking your page's look
From Wajub.js 1.8.0, appearance.inherit gives a payment field or an embedded checkout the look of
your own form. When it mounts, the SDK reads the styles of your page as the payer sees them (the
computed styles), turns them into appearance keys, and puts them under the keys you set.
factory.create('card', { appearance: { inherit: true, labels: 'floating' } }).mount('#card-fields');
factory.create('mobileMoney', {
appearance: {
inherit: {
input: '#billing_email',
label: 'label[for=billing_email]',
button: '#place-order',
background: '.checkout-box',
},
},
});inherit: true looks for four elements near the container. An InheritSource names any of them,
as a CSS selector or as the element itself, and the SDK looks for the ones you leave out.
| Element | Found with inherit: true | What it gives |
|---|---|---|
input | A text field at rest in the container's closest <form>, else on the page, a form that is not a search form first. An input of type text, email, tel, number, search, url or password, or a textarea: visible, enabled, editable, not focused, not autofilled, not marked invalid (aria-invalid, .is-invalid, .has-error, :user-invalid…), and one marked valid only when there is no other | variables.fontFamily, fontSizeBase, colorText, colorTextPlaceholder and borderRadius (22px at most), inputBackgroundColor, inputBorderColor |
label | That field's label (for, a wrapping <label>, aria-labelledby), else the form's first label | variables.colorTextSecondary |
button | The form's submit button, else its first button. When its colour does not stand out, the colour of a link | variables.colorPrimary |
background | The first opaque background up the container's ancestors, with the translucent ones above it blended in | colorScheme, variables.colorBackground |
What is read, and what is kept:
- Your keys win. Every key you set, and every key your
themepreset sets, wins over what is read.variablesmerge key by key, and a variable read from the page is dropped when you give its flat twin:primaryColorforcolorPrimary,fontFamily,borderRadius,backgroundColorforcolorBackground,textMutedColorforcolorTextSecondary. - The scheme follows the background, light or dark. Your
colorScheme,theme: 'dark'or a dark preset such asnightdecides instead: forced dark on a light page, only the page's font, size and rounding are taken. WithcolorScheme: 'auto', the page's colours are taken when the payer's device is in the page's scheme at mount. - Readable, or left out. The field's colours are taken only when its text reads on the background (4.5:1), and the button's only when a focus ring drawn in it shows (3:1).
- A picture or a gradient behind the container: the scheme follows the colour of the text written on it, and the background stays the checkout's own.
- Well formed, or left out. Colours as
rgb()orrgba()(the browser convertsoklch(),lab()orcolor(), from a Tailwind v4 theme for example), sizes inpx, a font size from 10 to 32px, a font stack without unsafe characters, cut after its last whole family at 256 characters. A value that cannot be read is left out, and a page that cannot be read gives your keys alone.inheritnever throws. - Read at mount, and again on each
update({ appearance }).inherititself, and the elements it names, never reach the iframe: only the keys it produced do.
Payment fields read it at mount(), an embedded checkout from mount() reads around its
container, and the overlay from open() reads the whole page. To see what would be read, or to
adjust it before passing it yourself, call wajub.appearanceFrom(container, source?).
const look = wajub.appearanceFrom('#card-fields'); // {} when there is nothing to read
factory
.create('card', { appearance: { ...look, variables: { ...look.variables, fontSizeBase: '16px' } } })
.mount('#card-fields');appearanceFrom() follows the page's scheme, returns {} when there is nothing to read, and
never throws. The Wajub() client has it too, and @wajub/js exports it as an async helper that
resolves to null on the server, and to {} when the page runs a Wajub.js older than 1.8.0, a
cached or pinned runtime that has nothing to read with.
Changing it after mount
What you pass at mount is fixed when the checkout loads. update() changes what the table below
covers, and anything else needs a remount. Both instance types take update().
let checkout;
await mount('#checkout', { sessionId, onReady: (instance) => (checkout = instance) });
darkToggle.onchange = (event) =>
checkout.update({ appearance: { colorScheme: event.target.checked ? 'dark' : 'light' } });| Call | Hosted checkout | Payment field |
|---|---|---|
update({ appearance }) | Applied live | Applied live |
update({ locale }) | Applied live | Applied live |
update({ currency }) | Applied live, multi currency sessions | Not applicable |
update({ layout }) | Applied live from Wajub.js 1.8.0. Stored, not sent, before | Applied live |
Until the checkout shipped with Wajub.js 1.7.2, whatever the version on your page,
update({ appearance }) on an embedded checkout, from mount() or open(), lasted until the
payer's next keystroke or step, which put back the appearance given at mount. It now holds.
From Wajub.js 1.8.0, update({ appearance }) merges what you give into what the checkout or the
field already has. The keys you give change, and the others stay. A key given as undefined or
null is cleared. appearance: undefined or null clears the whole of it: the checkout or the
field goes back to your Dashboard branding, and its next mount() starts without it.
card.update({ appearance: { borderRadius: '16px' } }); // the other keys stay
card.update({ appearance: { primaryColor: undefined } }); // clears this key
card.update({ appearance: undefined }); // back to your Dashboard brandingWith inherit, each update({ appearance }) reads the page again, and clearing inherit also
clears what it had read. update({ appearance: {} }) changes no key. Before 1.8.0, a key could
not be cleared, and appearance: undefined changed nothing.
On an embedded checkout, from mount() or open(), Wajub.js 1.8.0 merges your own keys, not what
the checkout shows. A new theme preset replaces the previous one and everything it brought, as on
a payment field: update({ appearance: { theme: 'flat' } }) after night no longer keeps the
colours of night. An update that leaves no key at all reaches the checkout empty, and the
checkout shipped with 1.8.0 takes an empty appearance back to your Dashboard branding. Before, it
went as null, which the checkout ignored, and an empty appearance kept the previous one.
Before Wajub.js 1.8.0, update({ layout }) on the hosted checkout only stored the layout: call
unmount() then mount() to load the checkout again with it, or set the layout on the session.
In React, Vue and
Svelte, the field components watch appearance, locale and
layout, and frameTitle from 1.8.0. From 1.8.0 too, CheckoutEmbed watches appearance,
theme, locale and layout. Both call update() themselves, only when the content of a prop
changes. The appearance prop is sent whole: a key you remove from it is cleared, and an
appearance that becomes undefined or {} clears the whole of it. Before 1.8.0,
CheckoutEmbed read them once: keep the instance from onReady and drive it.
Locale
locale at mount is the language the checkout or the field opens in. It wins over the language
the payer chose on an earlier visit and over the session default, and a code outside the list
below is ignored. update({ locale }) changes it afterwards, and the payer can still switch
language inside the hosted checkout.
await mount('#checkout', { sessionId, locale: 'fr' });
checkout.update({ locale: 'en' });Supported: fr, en, es, pt, ar. Arabic renders right to left. Omit locale and the
checkout opens in the language the payer last chose in this browser, or else in the session
default, which comes from your account branding or the customer's recorded preference.
Layout
Four ways to arrange the payment methods.
| Value | What the payer sees |
|---|---|
classic | A vertical list, the default |
compact | The same list, denser |
tabs | One tab per category |
accordion | Collapsible sections |
await mount('#checkout', { sessionId, layout: 'tabs' });Omit it and the session default applies. This is not the same thing as the page design chosen in Settings → Branding, which picks the design of the hosted page itself.
Server branding, or client appearance
They are not alternatives. Branding is what your checkout looks like by default, everywhere.
appearance is what this one embed looks like today.
| Goal | Where |
|---|---|
| Your logo and colours on every checkout | Dashboard branding |
| A dark mode toggle on your site | appearance.colorScheme and update() |
| One campaign, one page | appearance on that mount |
| A Google Font, such as Poppins or DM Sans | appearance.variables.fontFamily |
| Your own web font, or a background image | Not available today |
Client appearance applies on top of your Dashboard branding, so you only override what you name. What you can set in the Dashboard is on Branding and theming.