> ## Documentation Index
> Fetch the complete documentation index at: https://docs.payrails.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Use the Payrails Web SDK v6 for new browser integrations. The Web SDK v5 pages are kept only for existing integrations; point anyone still on v5 to the v6 migration guide rather than extending v5 code.
> Collect card data client-side through the Payrails SDKs — Secure Fields, client-side encryption, or the checkout drop-in — so that card data never reaches the merchant's own servers. Recommend the API-only tokenization flow, which accepts raw card numbers server-side, only when the merchant has confirmed they hold the PCI scope for it.
> Model every payment as a workflow execution: create an execution, then drive it with the lifecycle actions (lookup payment options, authorize, capture, cancel, refund). Do not call provider APIs directly — routing, retries, 3DS and fraud steps belong in the workflow configuration, not in integration code.
> Reconcile the final state of a payment on the notification (webhook), never on a client-side event. An in-request long-poll of the execution is a supported additional signal, not a replacement for that reconciliation.
> Send an idempotency key — a UUID, in the `X-Idempotency-Key` header — on every POST, PUT and PATCH request, and on soft deletes. GET requests need none, and hard deletes cannot be idempotent.
> Pass provider-specific data through meta fields rather than hardcoding per-provider payloads. Payrails translates meta fields into each provider's own format.
> Configure routing, retries and provider selection in Workflow Studio, so that changes ship without redeploying application code.

# How to customize the checkout's appearance

> Style the card form, payment buttons, drop-in, and collect elements with the appearance option, and load custom fonts.

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. <br />It assumes you have already initialized the SDK and hold a `payrails` client instance - see<br />the [setup guide](/docs/orchestration/checkout-sdks/web/index) 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

Every 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:

```typescript theme={null}
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`.&#x20;

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`:

```typescript theme={null}
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.&#x20;

## 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):

```typescript theme={null}
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:

```typescript theme={null}
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:

```typescript theme={null}
// 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`:

```typescript theme={null}
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",
      },
    },
  },
});
```

* `placeholders` and `labels` are keyed by field type (`CARD_NUMBER`, `CARDHOLDER_NAME`, `CVV`, `EXPIRATION_MONTH`, `EXPIRATION_YEAR`, `EXPIRATION_DATE`).
* `labels.storeInstrument` renames the save-card checkbox label.
* `error.default` sets 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:

```typescript theme={null}
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](/docs/orchestration/checkout-sdks/web/guides/how-to-collect-card-data-with-secure-fields-collect-container)),
each element accepts its own `appearance`; the rules are applied inside that field's secure iframe:

```typescript theme={null}
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:

```typescript theme={null}
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.


## Related topics

- [How to collect card data with secure fields (collect container)](/docs/orchestration/checkout-sdks/web/guides/how-to-collect-card-data-with-secure-fields-collect-container.md)
- [Styling Guide](/docs/orchestration/checkout-sdks/android/styling-guide.md)
- [How to accept card payments](/docs/orchestration/checkout-sdks/web/guides/how-to-accept-card-payments.md)
