Skip to content

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.

  1. A theme preset, if you name one.
  2. The flat keys, primaryColor and its neighbours.
  3. variables and rules, 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.

All three layers at once
await mount('#checkout', {
  sessionId,
  appearance: {
    theme: 'stripe',
    primaryColor: '#6366f1',
    colorScheme: 'auto',
    labels: 'floating',
    variables: { colorText: '#0f172a', fontSizeBase: '16px' },
    rules: { '.Input': { borderRadius: '8px' } },
  },
});

Presets

theme layers a set of flat keys under yours. These are the exact values.

PresetWhat it sets
stripeprimaryColor: #0570de, borderRadius: 4px, the system font stack, shadow: low
nightcolorScheme: dark, primaryColor: #7c3aed, backgroundColor: #0f172a, buttonTextColor: #f8fafc, inputBackgroundColor: #1e293b, inputBorderColor: #334155, textMutedColor: #94a3b8, borderRadius: 8px, shadow: none
flatshadow: none, borderRadius: 0
noneNothing. 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.

KeyTypeWhat it paints
primaryColorCSS colorThe accent, primary buttons, links
secondaryColorCSS colorSecondary accents
backgroundColorCSS colorThe surface behind the form
buttonTextColorCSS colorText on primary buttons
inputBackgroundColorCSS colorThe fields' own background. The page around them keeps its own
inputBorderColorCSS colorThe fields' own border. The page's other borders keep theirs
textMutedColorCSS colorHelper and secondary text
successColorCSS colorSuccess states and badges
errorColorCSS colorErrors and validation messages
fontFamilyCSS font stackThe whole checkout
borderRadiusCSS lengthDefault corner radius
shadowlow or noneElevation
colorSchemelight, dark, autolight or dark wins over the payer's device and the choice they saved on a hosted page. auto follows the device
labelsabove or floatingWhere field labels sit
disableAnimationsbooleanRemoves 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.

KeyWhat it sets
colorPrimaryThe accent colour
colorBackgroundThe surface behind the form
colorTextMain text, in the fields too
colorTextSecondarySecondary text, the field labels included
colorTextPlaceholderPlaceholder text
colorDangerError text
colorSuccessSuccess text
fontFamilyThe font stack. Its first family is loaded when it is a Google Font the checkout knows, such as Poppins or DM Sans
fontSizeBaseThe size of the text typed in the fields, at least 16px on a phone or a tablet. Labels and hints are a step smaller
fontWeightNormalRegular text weight
fontWeightMediumMedium text weight
fontWeightBoldBold text weight
spacingUnitThe base spacing unit
borderRadiusCorner radius
spacingGridRowThe gap between rows of fields
spacingGridColumnThe gap between columns of fields

Sixteen keys, and nothing else. A seventeenth is dropped on the way out.

Typography in one place
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 colorScheme you give applies from the first paint on every page, the embedded checkout and the page checkout() sends the payer to included, as it did in a payment field. When you give one, auto included, 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 fontFamily the 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.

Everything you can target
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.

GroupAllowed
Colourcolor, backgroundColor, opacity
Borderborder, borderColor, borderWidth, borderStyle, borderRadius
Boxpadding and its four sides, margin and its four sides
TypefontFamily, fontSize, fontWeight, fontStyle, lineHeight, letterSpacing, textAlign, textDecoration
EffectboxShadow, 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.

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.

Found near the container, or named
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.

ElementFound with inherit: trueWhat it gives
inputA 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 othervariables.fontFamily, fontSizeBase, colorText, colorTextPlaceholder and borderRadius (22px at most), inputBackgroundColor, inputBorderColor
labelThat field's label (for, a wrapping <label>, aria-labelledby), else the form's first labelvariables.colorTextSecondary
buttonThe form's submit button, else its first button. When its colour does not stand out, the colour of a linkvariables.colorPrimary
backgroundThe first opaque background up the container's ancestors, with the translucent ones above it blended incolorScheme, variables.colorBackground

What is read, and what is kept:

  • Your keys win. Every key you set, and every key your theme preset sets, wins over what is read. variables merge key by key, and a variable read from the page is dropped when you give its flat twin: primaryColor for colorPrimary, fontFamily, borderRadius, backgroundColor for colorBackground, textMutedColor for colorTextSecondary.
  • The scheme follows the background, light or dark. Your colorScheme, theme: 'dark' or a dark preset such as night decides instead: forced dark on a light page, only the page's font, size and rounding are taken. With colorScheme: '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() or rgba() (the browser converts oklch(), lab() or color(), from a Tailwind v4 theme for example), sizes in px, 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. inherit never throws.
  • Read at mount, and again on each update({ appearance }). inherit itself, 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?).

Read, adjust, pass
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().

A dark mode toggle on your own page
let checkout;

await mount('#checkout', { sessionId, onReady: (instance) => (checkout = instance) });

darkToggle.onchange = (event) =>
  checkout.update({ appearance: { colorScheme: event.target.checked ? 'dark' : 'light' } });
CallHosted checkoutPayment field
update({ appearance })Applied liveApplied live
update({ locale })Applied liveApplied live
update({ currency })Applied live, multi currency sessionsNot applicable
update({ layout })Applied live from Wajub.js 1.8.0. Stored, not sent, beforeApplied 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.

Change one key, clear one, clear all
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 branding

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

At mount, and afterwards
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.

ValueWhat the payer sees
classicA vertical list, the default
compactThe same list, denser
tabsOne tab per category
accordionCollapsible sections
Set it at mount
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.

GoalWhere
Your logo and colours on every checkoutDashboard branding
A dark mode toggle on your siteappearance.colorScheme and update()
One campaign, one pageappearance on that mount
A Google Font, such as Poppins or DM Sansappearance.variables.fontFamily
Your own web font, or a background imageNot 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.

What did you think of this content?