Styling Guide

The Payrails iOS SDK exposes two primary styling surfaces: the card form (via CardFormStylesConfig and CardFormConfig) and the pay button (via CardButtonStyle). Both use a merge-over-defaults system — you only need to specify the values you want to change.


Card form styling

Style hierarchy

CardFormStylesConfig
├── wrapperStyle: CardWrapperStyle      — outer container (border, background, padding)
├── errorTextStyle: CardStyle           — error message appearance
├── allInputFieldStyles: CardFieldSpecificStyles  — applied to every field
├── inputFieldStyles: [CardFieldType: CardFieldSpecificStyles]  — per-field overrides
├── labelStyles: [CardFieldType: CardStyle]  — per-field label overrides
├── fieldSpacing: CGFloat               — vertical gap between fields (default: 10)
└── sectionSpacing: CGFloat             — gap between sections (default: 16)

CardFieldSpecificStyles holds four state variants:

StateWhen applied
baseDefault idle state
focusField is active / first responder
completedValid value entered
invalidValidation failed

Basic example

let styles = CardFormStylesConfig(
    wrapperStyle: CardWrapperStyle(
        backgroundColor: .systemBackground,
        borderColor: .separator,
        borderWidth: 1,
        cornerRadius: 12,
        padding: UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
    ),
    errorTextStyle: Style(textColor: .systemRed),
    allInputFieldStyles: CardFieldSpecificStyles(
        base: Style(
            cornerRadius: 8,
            padding: UIEdgeInsets(top: 12, left: 12, bottom: 12, right: 12),
            borderWidth: 1,
            borderColor: .systemGray4,
            textColor: .label,
            font: .systemFont(ofSize: 16)
        ),
        focus: Style(borderColor: .systemBlue),
        completed: Style(borderColor: .systemGreen),
        invalid: Style(borderColor: .systemRed)
    ),
    fieldSpacing: 12,
    sectionSpacing: 20
)

let config = CardFormConfig(styles: styles)
let cardForm = Payrails.createCardForm(config: config)

Per-field overrides

Override the style for a specific field using inputFieldStyles. These are merged on top of allInputFieldStyles:

let styles = CardFormStylesConfig(
    allInputFieldStyles: CardFieldSpecificStyles(
        base: Style(borderWidth: 1, borderColor: .systemGray4, cornerRadius: 8)
    ),
    inputFieldStyles: [
        .CVV: CardFieldSpecificStyles(
            base: Style(backgroundColor: .systemGray6)
        ),
        .CARD_NUMBER: CardFieldSpecificStyles(
            base: Style(font: .monospacedDigitSystemFont(ofSize: 16, weight: .regular))
        )
    ]
)

Label styles

Control the appearance of field labels with labelStyles:

let styles = CardFormStylesConfig(
    labelStyles: [
        .CARD_NUMBER: Style(textColor: .label, font: .boldSystemFont(ofSize: 14)),
        .CVV: Style(textColor: .secondaryLabel, font: .systemFont(ofSize: 13))
    ]
)

Default values

PropertyDefault
wrapperStyle.borderColor.separator
wrapperStyle.borderWidth1.0
wrapperStyle.cornerRadius8.0
wrapperStyle.paddingUIEdgeInsets(16, 16, 16, 16)
allInputFieldStyles.base.cornerRadius2
allInputFieldStyles.base.paddingUIEdgeInsets(10, 10, 10, 10)
allInputFieldStyles.base.borderWidth1
allInputFieldStyles.focus.borderColor.systemBlue
allInputFieldStyles.completed.borderColor.systemGreen
allInputFieldStyles.invalid.borderColor.systemRed
errorTextStyle.textColor.systemRed
fieldSpacing10
sectionSpacing16

Field variants

FieldVariant controls the visual style of input fields:

let config = CardFormConfig(
    fieldVariant: .outlined  // default: bordered rectangle
    // or
    fieldVariant: .filled    // filled background, no border
)

Layout presets

CardLayoutConfig controls field arrangement:

PresetRows
.standard (default)[cardNumber], [cvv, expiryMonth, expiryYear]
.compact[cardNumber], [expiryMonth, expiryYear, cvv]
.minimal[cardNumber], [expiryMonth, expiryYear, cvv] (name hidden)
let config = CardFormConfig(
    layout: .compact
)

Combined expiry date field

Use useCombinedExpiryDateField: true to render a single MM/YY field instead of separate month/year:

let config = CardFormConfig(
    layout: CardLayoutConfig.preset(.compact, useCombinedExpiryDateField: true)
)

Custom layout

Define your own row arrangement:

let config = CardFormConfig(
    layout: CardLayoutConfig.custom(
        [[.CARD_NUMBER], [.EXPIRATION_DATE, .CVV]],
        useCombinedExpiryDateField: true
    )
)

Custom layouts must include .CARD_NUMBER, .CVV, and at least one expiry field (.EXPIRATION_DATE or both .EXPIRATION_MONTH + .EXPIRATION_YEAR). Invalid configurations fall back to the default layout.


Card icon alignment

let config = CardFormConfig(
    showCardIcon: true,
    cardIconAlignment: .left   // or .right
)

Pay button styling

Style the CardPaymentButton or GenericRedirectButton with CardButtonStyle:

let buttonStyle = CardButtonStyle(
    backgroundColor: .systemIndigo,
    textColor: .white,
    font: .boldSystemFont(ofSize: 16),
    cornerRadius: 10,
    borderWidth: 0,
    contentEdgeInsets: UIEdgeInsets(top: 14, left: 24, bottom: 14, right: 24),
    height: 52
)

let payButton = Payrails.createCardPaymentButton(
    buttonStyle: buttonStyle,
    translations: CardPaymenButtonTranslations(label: "Pay now")
)

CardButtonStyle properties

PropertyTypeDefault
backgroundColorUIColor?.systemBlue
textColorUIColor?.white
fontUIFont?System default
cornerRadiusCGFloat?8.0
borderWidthCGFloat?nil
borderColorUIColor?nil
contentEdgeInsetsUIEdgeInsets?nil
heightCGFloat?44

Stored instruments styling

StoredInstrumentsStyle controls the appearance of the stored instruments list:

let style = StoredInstrumentsStyle(
    backgroundColor: .clear,
    itemBackgroundColor: .systemBackground,
    selectedItemBackgroundColor: .systemGray6,
    labelTextColor: .label,
    labelFont: .systemFont(ofSize: 16),
    itemCornerRadius: 8,
    itemSpacing: 8,
    itemPadding: UIEdgeInsets(top: 12, left: 16, bottom: 12, right: 16),
    deleteButtonStyle: DeleteButtonStyle(
        backgroundColor: .systemRed,
        textColor: .white,
        cornerRadius: 4,
        size: CGSize(width: 32, height: 32)
    ),
    updateButtonStyle: UpdateButtonStyle(
        backgroundColor: .systemBlue,
        textColor: .white,
        cornerRadius: 4,
        size: CGSize(width: 32, height: 32)
    )
)

let instruments = Payrails.createStoredInstruments(style: style)

Translations

Override field labels, placeholders, and error messages:

let translations = CardTranslations(
    placeholders: CardTranslations.Placeholders(values: [
        .CARD_NUMBER: "Card number",
        .CVV: "Security code",
        .EXPIRATION_DATE: "MM / YY"
    ]),
    labels: CardTranslations.Labels(
        saveInstrument: "Remember this card"
    ),
    error: CardTranslations.ErrorMessages(values: [
        .CARD_NUMBER: "Please enter a valid card number",
        .CVV: "Invalid security code"
    ])
)

let config = CardFormConfig(translations: translations)

Override the pay button label:

let payButton = Payrails.createCardPaymentButton(
    translations: CardPaymenButtonTranslations(label: "Complete payment")
)

Translations are merged with the SDK defaults — you only need to provide the strings you want to change.