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

# Layout customization

> Rearrange the checkout using named slots on Web, Compose slot parameters on Android and SwiftUI `@ViewBuilder` slots on iOS.

Primer Checkout lets you customize the structure and layout of your checkout using platform-specific customization points.

You can rearrange components, insert your own content, and control how payment methods are displayed, while Primer continues to handle payment logic, state management, and security for you.

## How layout customization works

> [tab] Web
>
> Primer Checkout uses **named slots** within Web Components as the primary mechanism for layout customization. Slots are named placeholders in components where you can insert your own content or Primer components.
>
> > [!TIP]
> >
> > Slots are designated areas within Web Components where custom content can be inserted.
> > Each slot has a specific name (i.e. `main`) that determines where the content will appear.
> >
> > ```html theme={"dark"}
> > <primer-checkout client-token="your-client-token">
> >   <div slot="main">
> >     Your custom content goes here
> >   </div>
> > </primer-checkout>
> > ```
> >
> > When a component renders, it replaces each slot with the content you provide.
> > If you don't provide content for a slot, the component uses default content instead.

> [tab] Android
>
> Primer Checkout SDK uses standard Jetpack Compose **`@Composable` slot parameters** for layout customization. Every component provides a `Defaults` object with pre-built sub-components that you can selectively override.
>
> | Component | Defaults Object | Sub-components |
> | - | - | - |
> | `PrimerCheckoutSheet` | `PrimerCheckoutSheetDefaults` | `Splash`, `Loading`, `Success`, `Error`, `PaymentMethodSelection` |
> | `PrimerCardForm` | `CardFormDefaults` | `CardNumberField`, `ExpiryField`, `CvvField`, `CardholderField`, `SubmitButton` |
> | `PrimerPaymentMethods` | `PaymentMethodsDefaults` | `SectionHeader`, `Method`, `EmptyState` |

> [tab] iOS
>
> Primer Checkout SDK uses SwiftUI **`@ViewBuilder` slots** for layout customization. The composable views expose typed slot closures, and each component provides a `Defaults` namespace with pre-built slot bodies and per-field building blocks that you can selectively override.
>
> | Component | Defaults namespace | Slots |
> | - | - | - |
> | [`PrimerCardForm`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/card-form/primer-card-form) | [`CardFormDefaults`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/card-form/card-form-defaults) | `cardDetails`, `billingAddress`, `submitButton` |
> | [`PrimerPaymentMethods`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/payment-methods/primer-payment-methods) | [`PaymentMethodsDefaults`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/payment-methods/payment-methods-defaults) | `header`, `method`, `emptyState` |
> | [`PrimerVaultedPaymentMethods`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/payment-methods/vaulted-payment-methods) | `VaultedPaymentMethodsDefaults` | `header`, `item`, `submitButton` |
>
> The `PrimerCardForm` slots receive the [`PrimerCardFormSession`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/card-form/card-form-session), giving you access to its published `state` and actions such as `submit()`. The `PrimerPaymentMethods` `header` and `emptyState` slots receive the [`PrimerSelectionSession`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/payment-methods/payment-methods-session). The `method` slot receives the `CheckoutPaymentMethod` to render and an `onSelect` closure.
>
> `PrimerVaultedPaymentMethods` slots are plain closures that return `AnyView`, so wrap your content in `AnyView(...)`. Its `header` slot receives the `PrimerSelectionSession`. The `item` slot receives the saved method, `isSelected` and `onSelect`. The `submitButton` slot receives `isLoading`, `isEnabled` and `onSubmit`.

## Component hierarchy and customization points

> [tab] Web
>
> The checkout layout follows a hierarchical structure with slots at each level:
>
> slot: name
>
> Exposes a customizable slot
>
> → name
>
> Default content, replaced when you customize the slot
>
> > [accordion] \<primer-checkout> Component
> >
> > The root component that initializes the SDK and provides the checkout context.
> >
> > **Available Slots:**
> >
> > * `main` - The main content area for the checkout experience
> >
> > ```html theme={"dark"}
> > <primer-checkout client-token="your-token">
> >   <div slot="main">
> >     <!-- Your custom checkout UI -->
> >   </div>
> > </primer-checkout>
> > ```
>
> > [accordion] \<primer-main> Component (Optional)
> >
> > A pre-built component that manages checkout states and provides additional slots for customization.
> >
> > **Available Slots:**
> >
> > * `payments` - Contains payment method components
> > * `checkout-complete` - Content shown on successful payment
> > * `checkout-error` - Content shown when the SDK fails to initialize (e.g. invalid client token)
> >
> > ```html theme={"dark"}
> > <primer-checkout client-token="your-token">
> >   <primer-main slot="main">
> >     <div slot="payments">
> >       <!-- Your payment methods layout -->
> >     </div>
> >     <div slot="checkout-complete">
> >       <!-- Your success screen -->
> >     </div>
> >     <div slot="checkout-error">
> >       <!-- Your initialization-error screen -->
> >     </div>
> >   </primer-main>
> > </primer-checkout>
> > ```
> >
> > > [!NOTE]
> > >
> > > `checkout-error` covers SDK initialization failures only. Payment failures (declined cards, etc.) are handled separately — see [Error handling](https://primer.io/docs/checkout/primer-checkout/build-your-ui/error-handling.md).

> [tab] Android
>
> The checkout layout uses Compose slot parameters at each level. Use defaults for a complete checkout with no custom layout code, or override individual slots:

> [tab] iOS
>
> The checkout layout uses SwiftUI `@ViewBuilder` slots at each level. Use defaults for a complete checkout with no custom layout code, or override individual slots:

## Customization approaches

> [tab] Web
>
> ### Using `<primer-main>` with custom slots
>
> This approach allows you to customize specific parts of the checkout while relying on `<primer-main>` to handle state management:
>
> ```html theme={"dark"}
> <primer-checkout client-token="your-token">
>   <primer-main slot="main">
>     <div slot="payments">
>       <h2>Select Payment Method</h2>
>       <primer-payment-method type="PAYMENT_CARD"></primer-payment-method>
>       <primer-payment-method type="PAYPAL"></primer-payment-method>
>     </div>
>   </primer-main>
> </primer-checkout>
> ```
>
> > [!TIP]
> >
> > **Benefits of this approach:**
> >
> > * `<primer-main>` handles state transitions (loading, success, error)
> > * You only need to provide content for the slots you want to customize
> > * Default content is used for any slots you don't provide
>
> ### Fully custom implementation
>
> For complete control, you can bypass `<primer-main>` entirely and provide your own implementation. This requires handling state management yourself through events.
>
> For details on implementing fully custom layouts, see [Layout with Event Handling](https://primer.io/docs/checkout/primer-checkout/build-your-ui/advanced-layout-customization.md).

> [tab] Android
>
> ### Overriding one slot
>
> ```kotlin theme={"dark"}
> PrimerCheckoutSheet(
>     checkout = checkout,
>     success = { checkoutData ->
>         MyCustomSuccessScreen(checkoutData)
>     },
> )
> ```
>
> ### Using standalone components with CheckoutHost
>
> `PrimerCheckoutHost` lets you embed checkout components inline (without a modal sheet), giving you full control over layout:
>
> ```kotlin theme={"dark"}
> PrimerCheckoutHost(checkout = checkout) {
>     Column(modifier = Modifier.padding(16.dp)) {
>         val cardFormController = rememberCardFormController(checkout)
>         PrimerCardForm(controller = cardFormController)
>
>         Spacer(modifier = Modifier.height(16.dp))
>
>         val paymentMethodsController = rememberPaymentMethodsController(checkout)
>         PrimerPaymentMethods(controller = paymentMethodsController)
>     }
> }
> ```
>
> ### Mixing defaults with custom content
>
> ```kotlin theme={"dark"}
> val cardFormController = rememberCardFormController(checkout)
>
> PrimerCardForm(
>     controller = cardFormController,
>     cardDetails = {
>         CardFormDefaults.CardDetailsContent(cardFormController)
>     },
>     submitButton = {
>         MyBrandedPayButton(
>             onClick = { cardFormController.submit() },
>         )
>     },
> )
> ```
>
> ### Modifier support
>
> All components accept a `Modifier` parameter for standard Compose layout control:
>
> ```kotlin theme={"dark"}
> PrimerCardForm(
>     controller = cardFormController,
>     modifier = Modifier
>         .fillMaxWidth()
>         .padding(16.dp),
> )
> ```

> [tab] iOS
>
> ### Overriding one slot
>
> Pass a labeled slot argument and leave the others at their defaults. Here only the submit button is replaced — `cardDetails` and `billingAddress` keep the default styling:
>
> ```swift theme={"dark"}
> PrimerCardForm(submitButton: { session in
>     MyBrandedPayButton(isLoading: session.state.isLoading) {
>         session.submit()
>     }
> })
> ```
>
> ### Using composable views inline
>
> Hold a `PrimerCheckoutSession` as a `@StateObject` and attach it with the `.primerCheckoutSession(_:theme:onCompletion:)` modifier to embed checkout views inline (without the managed modal), giving you full control over layout:
>
> ```swift theme={"dark"}
> struct CheckoutScreen: View {
>     @StateObject private var session: PrimerCheckoutSession
>
>     init(clientToken: String) {
>         _session = StateObject(wrappedValue: PrimerCheckoutSession(clientToken: clientToken))
>     }
>
>     var body: some View {
>         ScrollView {
>             VStack(spacing: 16) {
>                 PrimerCardForm()
>                 PrimerPaymentMethods()
>             }
>             .padding()
>         }
>         .primerCheckoutSession(session) { state in
>             handle(state)
>         }
>     }
> }
> ```
>
> ### Mixing defaults with custom content
>
> Call a `CardFormDefaults` helper inside a slot to keep the SDK's default rendering while wrapping it in your own layout:
>
> ```swift theme={"dark"}
> PrimerCardForm(
>     cardDetails: { session in
>         VStack(alignment: .leading, spacing: 16) {
>             CardFormDefaults.cardDetails(session)
>             MyPromoBanner()
>         }
>     },
>     submitButton: { session in
>         MyBrandedPayButton(isLoading: session.state.isLoading) {
>             session.submit()
>         }
>     }
> )
> ```
>
> ### Recomposing individual fields
>
> Assemble the card section from the per-field building blocks on [`CardFormDefaults`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/card-form/card-form-defaults). The card fields always render. Each billing-address building block renders only when your Dashboard billing-address configuration includes its field:
>
> ```swift theme={"dark"}
> PrimerCardForm(cardDetails: { session in
>     VStack(spacing: 16) {
>         CardFormDefaults.cardNumber(session)
>         HStack(spacing: 16) {
>             CardFormDefaults.expiryDate(session)
>             CardFormDefaults.cvv(session)
>         }
>         CardFormDefaults.cardholderName(session)
>     }
> })
> ```

## Why customization points matter

> [tab] Web
>
> Slot names are crucial for several reasons:
>
> 1. **Component targeting** - Names tell the component exactly where to insert your content
> 2. **Default content** - Components can provide default content for slots that aren't filled
> 3. **Preventing accidental rendering** - Content without a matching slot won't be displayed
> 4. **Multiple insertion points** - Different named slots allow multiple insertion points
>
> Using the wrong slot name or omitting it entirely can lead to content not appearing where expected.

> [tab] Android
>
> Compose slot parameters serve the same purpose as Web slots:
>
> 1. **Component targeting** - Named lambda parameters define exactly which part of the UI you're customizing
> 2. **Default content** - `Defaults` objects provide pre-built content for any slots you don't override
> 3. **Type safety** - The compiler enforces that your custom content matches the expected slot signature
>
> Passing content to the wrong parameter won't compile, preventing layout mistakes at build time.

> [tab] iOS
>
> SwiftUI `@ViewBuilder` slots provide similar benefits to Web slots and Android slot parameters:
>
> 1. **Component targeting** - Each labeled slot closure defines exactly which part of the UI you're customizing
> 2. **Default content** - `Defaults` namespaces provide pre-built bodies for any slot you don't override
> 3. **Type safety** - Slot closures receive typed arguments. Section slots receive the relevant session ([`PrimerCardFormSession`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/card-form/card-form-session) or [`PrimerSelectionSession`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/payment-methods/payment-methods-session)). Per-item slots receive the item and its callbacks, for example `method` receives the `CheckoutPaymentMethod` and `onSelect`.
>
> When replacing a slot, your closure receives everything needed to build a functional replacement, including the session, the item, its state and its callbacks.

## Styling custom layouts

> [tab] Web
>
> When styling custom layouts, use CSS variables for consistency:
>
> ```css theme={"dark"}
> .payment-section {
>   padding: var(--primer-space-medium);
>   border-radius: var(--primer-radius-small);
>   background-color: var(--primer-color-background-outlined-default);
> }
>
> .payment-section h2 {
>   color: var(--primer-color-text-primary);
>   font-family: var(--primer-typography-title-large-font);
>   font-size: var(--primer-typography-title-large-size);
> }
> ```
>
> Using these properties ensures your custom layout maintains visual consistency with the checkout components.
>
> For detailed information on available components and their slots, refer to the component SDK Reference documentation:
>
> * [Checkout Component](https://primer.io/docs/sdk/primer-checkout-web/components/primer-checkout.md)
> * [Main Component](https://primer.io/docs/sdk/primer-checkout-web/components/primer-main.md)
> * [Payment Method Component](https://primer.io/docs/sdk/primer-checkout-web/components/primer-payment-method.md)

> [tab] Android
>
> Use `PrimerTheme` design tokens to maintain visual consistency with checkout components. See [Styling Customization](https://primer.io/docs/checkout/primer-checkout/build-your-ui/styling-customization.md) for details.

> [tab] iOS
>
> Use `PrimerCheckoutTheme` design tokens to maintain visual consistency with checkout components. Visual styling is theme-driven through these design tokens rather than per-field configuration. See [Styling Customization](https://primer.io/docs/checkout/primer-checkout/build-your-ui/styling-customization.md) for details.

## Try it live in StackBlitz

See these layout patterns working in your browser:

<table>
  <thead>
    <tr>
      <th width="200">Example</th>
      <th>Description</th>

      <th width="150" />
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>**Custom sectioned layout**</td>
      <td>Payment methods organized into sections (Card, Quick Checkout, Alternative Methods)</td>

      <td>
        [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/fork/github/primer-io/examples/tree/main/examples/primer-checkout-custom-layout)
      </td>
    </tr>

    <tr>
      <td>**Custom form layout**</td>
      <td>Reordered card form components with custom styling and layout</td>

      <td>
        [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/fork/github/primer-io/examples/tree/main/examples/primer-checkout-custom-form)
      </td>
    </tr>
  </tbody>
</table>

## See also

> [card] [Slot architecture explorer](https://primer.io/docs/checkout/primer-checkout/build-your-ui/slot-architecture-explorer.md)
>
> Explore the full component hierarchy and all available slots interactively

> [card] [Layout with event handling](https://primer.io/docs/checkout/primer-checkout/build-your-ui/advanced-layout-customization.md)
>
> Learn about events, dynamic rendering, and fully custom implementations

> [card] [Error handling](https://primer.io/docs/checkout/primer-checkout/build-your-ui/error-handling.md)
>
> Handle payment failures and display error messages

> [card] [Design tokens explorer](https://primer.io/docs/checkout/primer-checkout/build-your-ui/design-tokens-explorer.md)
>
> Explore CSS variables and see which components they affect

> [card] [Render outside the checkout](https://primer.io/docs/sdk/primer-checkout-web/components/primer-provider.md)
>
> Use Primer components elsewhere on the page with primer-provider


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.