Your checkout, down to the pixel.
The Naturpay checkout is a blank surface you paint with your own design system. Drive type, color, spacing, radius and motion through a single appearance object, then drop to per-element CSS for the last ten percent. Nothing here looks bolted on, because nothing here is.
The Appearance API
One theme object, five token families
Most of the design work lives in a single appearance object you pass at mount. Five families of design tokens — type, color, spacing, radius and motion — cascade across every field, label, button and wallet inside the checkout. Change one value and the whole surface repaints in place, with no remount and no layout jump.
- Type tokens: font family, weight scale, label and input sizes, letter spacing.
- Color tokens: accent, surface, text, muted, border, danger and success states.
- Spacing, radius and motion tokens for rhythm, corners and transition timing.
export const appearance = { variables: { // type fontFamily: 'Familjen Grotesk, sans-serif', fontSizeBase: '15px', labelWeight: 600, // color colorAccent: '#c2410c', colorSurface: '#16120f', colorText: '#efe7da', colorMuted: '#8f8474', colorBorder: '#3a2f26', colorDanger: '#d96a4a', // spacing · radius · motion spacingUnit: '6px', radius: '4px', motion: '160ms ease', }, };
The last ten percent
When tokens stop, raw CSS starts
Tokens carry the broad strokes. For everything else there is a rules map keyed by stable class names and state selectors, so you can style a focused field, a card error or a hovered wallet exactly as your design system demands. Switch the panels to see tokens, per-element rules and a full dark theme side by side.
// fast path — set the broad strokes once const theme = { variables: { fontFamily: 'Familjen Grotesk, sans-serif', colorAccent: '#c2410c', colorSurface: '#16120f', radius: '4px', spacingUnit: '6px', }, };
// last 10% — per-element rules and state selectors const theme = { rules: { '.Label': { textTransform: 'uppercase', letterSpacing: '.04em' }, '.Field': { border: '1px solid #3a2f26', padding: '12px' }, '.Field:focus': { border: '1px solid #c2410c', shadow: '0 0 0 3px rgba(194,65,12,.18)' }, '.Field--invalid': { border: '1px solid #d96a4a' }, '.WalletButton:hover': { transform: 'translateY(-1px)' }, '.PayButton': { borderRadius: '3px', fontWeight: 600 }, }, };
// a full dark theme is just another variable set const dark = { variables: { colorSurface: '#16120f', colorText: '#efe7da', colorMuted: '#8f8474', colorBorder: '#3a2f26', colorAccent: '#e2570f', }, }; const isDark = matchMedia('(prefers-color-scheme: dark)').matches; checkout.update({ appearance: isDark ? dark : light });
One continuous product
It reads like your design system
Because the checkout draws from the same tokens as the rest of your app, the payment step feels like the page it lives on, not a window punched into it. Buyers never cross a visible seam, and you never ship a screen that breaks your own guidelines.
- Pull tokens straight from your CSS custom properties or design library.
- Match focus rings, error styling and button shape to your existing forms.
- Fields stay isolated and tokenized, so styling never touches PCI scope.
Runtime theming
Switch light, dark or brand on the fly
Theming is not frozen at mount. Call update with a new appearance object whenever your app changes mode, and the live checkout repaints in place — same DOM, same input state, same focused field. Follow the system color scheme, honor a user toggle, or swap a sub-brand palette per route.
- React to prefers-color-scheme and to a manual light or dark toggle.
- Carry a different palette per brand or per storefront from one integration.
- No remount, no flash, no lost keystrokes when the theme changes mid-flow.
// follow a user toggle toggle.addEventListener('change', () => { checkout.update({ appearance: themes.light }); });
// react to the system color scheme matchMedia('(prefers-color-scheme: dark)') .addEventListener('change', (e) => { checkout.update({ appearance: e.matches ? themes.dark : themes.light, }); });
// swap a sub-brand palette per route router.on('enter', (route) => { checkout.update({ appearance: themes[route.brand] ?? themes.base, }); });
const checkout = wv.checkout({ intent: 'pi_3Q…', appearance, // localize copy and number formats locale: 'fr-FR', // show the buyer's currency, settle in yours display: { currency: 'EUR', presentment: 'auto', }, }); // override a single string if you must checkout.setLabels({ pay: 'Régler la commande' });
Localization and currency
Speak the buyer's language and money
Set a locale and the checkout translates its labels, errors and field hints, and formats numbers, dates and amounts to local convention. Display prices in the shopper's currency while you settle in your own, and override any single string when your brand voice needs a specific word.
- Built-in translations for the checkout UI across major locales.
- Locale-aware amount, date and decimal formatting out of the box.
- Per-label overrides so any string can match your exact wording.
Layout options
Arrange the flow your way
Beyond color and type, the structure of the payment step is yours to set. Pick a layout that fits the surface — a calm single column, a stepped accordion, or fields laid inline inside a form you already own.
One column
A single, stacked flow that suits most pages: wallet on top, card fields below, one clear call to pay. The simplest layout to read and the fastest to scan on mobile.
layout: 'stacked'Accordion
Group the steps into expandable sections — contact, address, payment — so a longer flow stays calm. One panel opens at a time, with progress kept clear the whole way down.
layout: 'accordion'Inline fields
Mount the card, expiry and CVC fields individually inside a form you already built. Naturpay owns only the sensitive inputs; the grid, the labels and the order are entirely yours.
layout: 'inline'Make the checkout unmistakably yours
Grab a test key, pass your tokens, and watch the payment step take on your brand in a single update call. When the design is dialed in, talk to an engineer about going live.