How to customize the checkout's appearance
This guide shows you how to style the pre-built card form, payment buttons, drop-in, and individual collect elements with the appearance option, how to change their labels and placeholders, and how to load custom fonts.
It assumes you have already initialized the SDK and hold a payrails client instance - see
the setup guide if you have not.
1. The base stylesheet
The pre-built components (drop-in, card form, buttons, card list) ship a base stylesheet. It is injected automatically when the SDK loads - no manual import is needed.
Everything below customizes on top of these base styles.
2. The appearance option
appearance optionEvery element the SDK draws accepts an appearance object. Its rules field is CSS-like key/value: selectors on the outside, CSS declarations on the inside:
const paymentButton = payrails.paymentButton({
appearance: {
rules: {
'.payrails-button': { backgroundColor: '#1a1a1a', color: '#fff' },
'.payrails-button:hover': { backgroundColor: '#333' },
'.payrails-button--disabled': { opacity: '0.5' },
},
},
});Selectors target stable class names the SDK guarantees on the DOM:
.payrails-input, .payrails-button, .payrails-dropdown, .payrails-label, .payrails-tile, .payrails-container, .payrails-row, .payrails-cell, .payrails-icon, .payrails-checkbox, .payrails-error, .payrails-text.
State variants use BEM modifiers on those classes:
| State | Class |
|---|---|
| Field with invalid input | .payrails-input--invalid |
| Field with valid input | .payrails-input--valid |
| Field is empty | .payrails-input--empty |
| Field touched since load | .payrails-input--dirty |
| Button in loading state | .payrails-button--loading |
| Button disabled | .payrails-button--disabled |
| Checkbox checked | .payrails-checkbox--checked |
| Brand tile selected | .payrails-tile--selected |
Any selector a browser supports is valid inside rules — pseudo-classes and pseudo-elements (:hover, :focus, :focus-visible, ::placeholder, ::selection), media queries (@media), and feature queries (@supports).
The --invalid and --valid classes clear while a field is focused, so a field being corrected does not show the error state. For a persistent invalid look on touched fields, key off --dirty:
rules: {
'.payrails-input--dirty:not(:focus).payrails-input--invalid': {
borderColor: '#dc2626',
},
}Rules are scoped to the element instance that declares them — a rule passed to one card form does not leak into another widget. SDK defaults live in the CSS layer @layer payrails-defaults; your rules land in
@layer payrails-appearance, which always wins over the defaults.
3. Style the card form
payrails.cardForm(options) accepts a CardFormAppearance — root rules for the form itself plus one slot per nested widget (installments dropdown, address selector, brand selector):
const cardForm = payrails.cardForm({
appearance: {
rules: {
'.payrails-container': { maxWidth: '480px', gap: '16px' },
'.payrails-input': {
border: '1px solid #eae8ee',
borderRadius: '4px',
color: '#1d1d1d',
},
'.payrails-input:focus': { borderColor: '#6b7cff' },
'.payrails-input--invalid': {
color: '#f44336',
backgroundColor: '#fdeaea',
},
'.payrails-label': { fontSize: '12px', fontWeight: 'bold' },
'.payrails-error': { color: '#f44336' },
},
installments: {
rules: { '.payrails-dropdown': { borderRadius: '8px' } },
},
address: {
rules: { '.payrails-dropdown, .payrails-input': { borderRadius: '8px' } },
},
brandSelector: {
rules: { '.payrails-tile--selected': { borderColor: '#4F46E5' } },
},
},
});
cardForm.mount('#card-form');The card payment button is a sibling of the card form, not a child — pass its appearance to payrails.paymentButton({ appearance }) directly, or to the cardPaymentButton slot of the drop-in's appearance.
4. Style the drop-in
The drop-in's appearance is keyed by its building blocks; each slot takes the same shape as the standalone element:
const dropin = payrails.dropin({
appearance: {
rules: {
'.payrails-container': { borderRadius: '12px' },
},
cardForm: {
rules: { '.payrails-input': { border: '1px solid #ddd' } },
},
cardPaymentButton: {
rules: { '.payrails-button': { backgroundColor: '#4F46E5' } },
},
loadingScreen: { rules: {} },
authSuccess: { rules: {} },
authFailed: { rules: {} },
},
});5. Provider-drawn wallet buttons keep their own options
Google Pay and PayPal draw their own chrome (a native SDK button, a hosted iframe), so CSS rules can't reach them. They take structured appearance.settings instead:
// Google Pay
const googlePayButton = payrails.googlePayButton({
appearance: {
settings: {
buttonColor: 'black', // per Google Pay ButtonColor
buttonType: 'buy', // per Google Pay ButtonType
buttonSizeMode: 'fill',
locale: 'de',
},
},
});
// PayPal
const paypalButton = payrails.paypalButton({
appearance: {
settings: {
color: 'gold', // 'gold' | 'blue' | 'silver' | 'white' | 'black'
shape: 'rect', // 'rect' | 'pill'
label: 'paypal',
height: 40,
tagline: false,
locale: 'de_DE',
},
},
});6. Change labels, placeholders, and error messages
Text on the card form is controlled through translations:
const cardForm = payrails.cardForm({
showStoreInstrumentCheckbox: true,
translations: {
placeholders: {
CARD_NUMBER: '1234 1234 1234 1234',
CVV: 'CVC',
},
labels: {
CARD_NUMBER: 'Card number',
CVV: 'Security code',
storeInstrument: 'Save this card for next time',
},
error: {
default: {
CARD_NUMBER: 'Please check the card number',
},
},
},
});placeholdersandlabelsare keyed by field type (CARD_NUMBER,CARDHOLDER_NAME,CVV,EXPIRATION_MONTH,EXPIRATION_YEAR,EXPIRATION_DATE).labels.storeInstrumentrenames the save-card checkbox label.error.defaultsets the default validation error message per field type.
The payment button's label is translations.label on payrails.paymentButton(...).
7. Load custom fonts
The secure fields render inside iframes, so fonts from your page are not automatically available there. Pass font descriptors via fonts — either a CSS source or an explicit font-face definition:
const cardForm = payrails.cardForm({
fonts: [
{
family: 'Inter',
src: "url('https://example.com/fonts/inter.woff2')",
weight: 400,
style: 'normal',
},
],
appearance: {
rules: {
'.payrails-input': { fontFamily: 'Inter, sans-serif' },
},
},
});A font descriptor accepts family, src, style, weight, unicodeRange, display, or a cssSrc URL. The same fonts option is accepted by payrails.collectContainer(...).
8. Style individual collect elements
When you build your own form with a collect container (see How to collect card data with secure fields),
each element accepts its own appearance; the rules are applied inside that field's secure iframe:
const cardNumber = container.createCollectElement({
type: ElementType.CARD_NUMBER,
appearance: {
rules: {
'.payrails-input': {
border: '1px solid #eae8ee',
padding: '10px 16px',
borderRadius: '4px',
color: '#1d1d1d',
},
'.payrails-input--valid': { color: '#4caf50' },
'.payrails-input--invalid': { color: '#f44336' },
'.payrails-label': { fontSize: '12px', fontWeight: 'bold' },
'.payrails-error': { color: '#f44336' },
},
},
});Only the root rules are forwarded to the iframe — secure fields have no nested widget slots.
9. Update appearance and texts after mounting
The card form can be restyled and re-labeled at runtime without remounting:
cardForm.update({
appearance: {
rules: { '.payrails-input': { borderColor: '#6b7cff' } },
},
translations: {
labels: { CARD_NUMBER: 'Kartennummer' },
},
});Updates accumulate: a partial appearance merges over the current rules, leaving other selectors and nested widget slots (installments, address, brandSelector) intact. To clear a declaration, pass its key with an empty value; clearing a whole selector requires a remount.
The card payment button supports paymentButton.update({ translations }) for its label — its appearance cannot be changed after mounting.
Updated 1 day ago