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

> Learn how to customize the structure and layout of your checkout experience.

export const ANDROID_LAYOUT_HIERARCHY = [{
  name: 'PrimerCheckoutSheet',
  note: 'Composable',
  children: [{
    name: 'splash',
    note: 'slot',
    goesInto: 'PrimerCheckoutSheetDefaults.Splash()'
  }, {
    name: 'loading',
    note: 'slot',
    goesInto: 'PrimerCheckoutSheetDefaults.Loading()'
  }, {
    name: 'paymentMethodSelection',
    note: 'slot',
    goesInto: 'PrimerCheckoutSheetDefaults.PaymentMethodSelection()',
    children: [{
      name: 'PrimerVaultedPaymentMethods',
      note: 'saved payment methods',
      children: [{
        name: 'header',
        note: 'slot',
        goesInto: 'VaultedPaymentMethodsDefaults.SectionHeader()'
      }, {
        name: 'item',
        note: 'slot',
        goesInto: 'VaultedPaymentMethodsDefaults.Method()'
      }, {
        name: 'submitButton',
        note: 'slot',
        goesInto: 'VaultedPaymentMethodsDefaults.SubmitButton()'
      }]
    }, {
      name: 'PrimerPaymentMethods',
      note: 'available payment methods',
      children: [{
        name: 'header',
        note: 'slot',
        goesInto: 'PaymentMethodsDefaults.SectionHeader()'
      }, {
        name: 'method',
        note: 'slot, per item',
        goesInto: 'PaymentMethodsDefaults.Method()'
      }]
    }]
  }, {
    name: 'cardForm',
    note: 'slot',
    goesInto: 'PrimerCardForm',
    children: [{
      name: 'cardDetails',
      note: 'slot',
      goesInto: 'CardFormDefaults.CardDetailsContent()',
      children: [{
        name: 'CardNumberField'
      }, {
        name: 'ExpiryField'
      }, {
        name: 'CvvField'
      }, {
        name: 'CardholderField'
      }, {
        name: 'CardNetworkField',
        note: 'co-badge only'
      }]
    }, {
      name: 'billingAddress',
      note: 'slot',
      goesInto: 'CardFormDefaults.BillingAddressContent()',
      children: [{
        name: 'CountryCodeField'
      }, {
        name: 'FirstNameField'
      }, {
        name: 'LastNameField'
      }, {
        name: 'AddressLine1Field'
      }, {
        name: 'AddressLine2Field'
      }, {
        name: 'CityField'
      }, {
        name: 'StateField'
      }, {
        name: 'PostalCodeField'
      }]
    }, {
      name: 'submitButton',
      note: 'slot',
      goesInto: 'CardFormDefaults.SubmitButton()'
    }]
  }, {
    name: 'success',
    note: 'slot',
    goesInto: 'PrimerCheckoutSheetDefaults.Success()'
  }, {
    name: 'error',
    note: 'slot',
    goesInto: 'PrimerCheckoutSheetDefaults.Error()'
  }]
}];

export const IOS_LAYOUT_HIERARCHY = [{
  name: 'PrimerCheckout',
  note: 'SwiftUI View',
  children: [{
    name: 'PrimerCheckoutScope',
    note: 'root scope',
    children: [{
      name: 'container',
      note: 'custom container view'
    }, {
      name: 'splashScreen',
      note: 'splash screen override'
    }, {
      name: 'loadingScreen',
      note: 'loading screen override'
    }, {
      name: 'errorScreen',
      note: 'error screen override'
    }, {
      name: 'PrimerPaymentMethodSelectionScope',
      goesInto: '.paymentMethodSelection',
      children: [{
        name: 'screen',
        note: 'full selection screen'
      }, {
        name: 'paymentMethodItem',
        note: 'individual method items'
      }, {
        name: 'categoryHeader',
        note: 'section headers'
      }, {
        name: 'emptyStateView',
        note: 'empty state'
      }]
    }, {
      name: 'PrimerCardFormScope',
      goesInto: 'getPaymentMethodScope(.self)',
      children: [{
        name: 'screen',
        note: 'full card form screen'
      }, {
        name: 'cardInputSection',
        note: 'card number, expiry, CVV group'
      }, {
        name: 'billingAddressSection',
        note: 'address fields group'
      }, {
        name: 'submitButton',
        note: 'submit button override'
      }, {
        name: 'errorScreen',
        note: 'error screen override'
      }, {
        name: '[field]Config',
        note: 'per-field configs (InputFieldConfig)'
      }, {
        name: 'PrimerSelectCountryScope',
        goesInto: '.selectCountry',
        children: [{
          name: 'countryItem',
          note: 'individual country items'
        }]
      }]
    }, {
      name: 'PrimerWebRedirectScope',
      goesInto: 'getPaymentMethodScope(.self)',
      children: [{
        name: 'screen',
        note: 'full screen replacement'
      }, {
        name: 'payButton',
        note: 'custom pay button'
      }]
    }, {
      name: 'PrimerFormRedirectScope',
      goesInto: 'getPaymentMethodScope(.self)',
      children: [{
        name: 'screen',
        note: 'full screen replacement'
      }, {
        name: 'formSection',
        note: 'custom form fields area'
      }, {
        name: 'submitButton',
        note: 'custom submit button'
      }]
    }, {
      name: 'PrimerQRCodeScope',
      goesInto: 'getPaymentMethodScope(.self)',
      children: [{
        name: 'screen',
        note: 'full screen replacement'
      }]
    }]
  }]
}];

export const LAYOUT_HIERARCHY = [{
  name: 'primer-checkout',
  slots: ['main'],
  children: [{
    name: 'primer-main',
    goesInto: 'main',
    slots: ['payments', 'checkout-complete'],
    children: [{
      name: 'primer-vault-manager',
      goesInto: 'payments',
      note: 'saved payment methods'
    }, {
      name: 'primer-show-other-payments',
      goesInto: 'payments',
      slots: ['other-payments'],
      children: [{
        name: 'primer-payment-method',
        goesInto: 'other-payments',
        note: '× N',
        children: [{
          name: 'primer-card-form',
          note: 'when type="PAYMENT_CARD"',
          slots: ['card-form-content'],
          children: [{
            name: 'primer-input-card-number',
            goesInto: 'card-form-content'
          }, {
            name: 'primer-input-card-expiry',
            goesInto: 'card-form-content'
          }, {
            name: 'primer-input-cvv',
            goesInto: 'card-form-content'
          }, {
            name: 'primer-input-card-holder-name',
            goesInto: 'card-form-content'
          }, {
            name: 'primer-billing-address',
            goesInto: 'card-form-content'
          }, {
            name: 'primer-card-form-submit',
            goesInto: 'card-form-content'
          }]
        }]
      }]
    }, {
      name: 'primer-error-message-container',
      goesInto: 'payments'
    }, {
      name: 'primer-checkout-complete',
      goesInto: 'checkout-complete'
    }]
  }]
}];

export const ComponentTree = ({nodes, showLegend = true}) => {
  const renderNode = (node, depth = 0, isLast = true, prefix = '') => {
    const connector = depth === 0 ? '' : isLast ? '\u2514\u2500\u2500' : '\u251c\u2500\u2500';
    const childPrefix = prefix + (depth === 0 ? '' : isLast ? '    ' : '\u2502   ');
    const hasSlots = node.slots && node.slots.length > 0;
    const goesInto = node.goesInto;
    return <div key={node.name + depth} style={{
      fontFamily: 'var(--font-mono, monospace)',
      fontSize: '14px',
      lineHeight: '2'
    }}>
        <div style={{
      display: 'flex',
      alignItems: 'center',
      whiteSpace: 'nowrap'
    }}>
          <span className="component-tree-connector">{prefix}{connector}</span>
          {depth > 0 && <span style={{
      width: '8px'
    }} />}
          <span className="component-tree-name">{node.name}</span>
          {goesInto && <span className="component-tree-goes-into">
              {'\u2192'} {goesInto}
            </span>}
          {hasSlots && node.slots.map(slotName => <span key={slotName} className="component-tree-slot">
              slot: {slotName}
            </span>)}
          {node.note && <span className="component-tree-note">
              {node.note}
            </span>}
        </div>
        {node.children && node.children.map((child, i) => renderNode(child, depth + 1, i === node.children.length - 1, childPrefix))}
      </div>;
  };
  return <div className="component-tree-container">
      <div>
        {nodes.map((node, i) => renderNode(node, 0, i === nodes.length - 1, ''))}
      </div>
      {showLegend && <div className="component-tree-legend">
          <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '6px'
  }}>
            <span className="component-tree-slot">
              slot: name
            </span>
            <span>Exposes a customizable slot</span>
          </div>
          <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '6px'
  }}>
            <span className="component-tree-goes-into">
              {'\u2192'} name
            </span>
            <span>Default content, replaced when you customize the slot</span>
          </div>
        </div>}
    </div>;
};

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

<Tabs>
  <Tab title="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.
    </Tip>
  </Tab>

  <Tab title="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>

  <Tab title="iOS">
    Primer Checkout SDK uses **scope-based customization** via closures. Each scope exposes properties for replacing individual fields, sections, or entire screens with custom SwiftUI views.

    | Scope                               | Customization points                                                                                |
    | ----------------------------------- | --------------------------------------------------------------------------------------------------- |
    | `PrimerCheckoutScope`               | `container`, `splashScreen`, `loadingScreen`, `errorScreen`                                         |
    | `PrimerPaymentMethodSelectionScope` | `screen`, `paymentMethodItem`, `categoryHeader`, `emptyStateView`                                   |
    | `PrimerCardFormScope`               | `screen`, `cardInputSection`, `billingAddressSection`, `submitButton`, `errorScreen`, field configs |
    | `PrimerWebRedirectScope`            | `screen`, `payButton`, `submitButtonText`                                                           |
    | `PrimerFormRedirectScope`           | `screen`, `formSection`, `submitButton`, `submitButtonText`                                         |
    | `PrimerQRCodeScope`                 | `screen`                                                                                            |
  </Tab>
</Tabs>

## Component hierarchy and customization points

<Tabs>
  <Tab title="Web">
    The checkout layout follows a hierarchical structure with slots at each level:

    <ComponentTree nodes={LAYOUT_HIERARCHY} />

    <Accordion title="<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>

    <Accordion title="<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

      ```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>
        </primer-main>
      </primer-checkout>
      ```

      <Info>
        Error states are managed by the parent `<primer-checkout>` component, not by `<primer-main>`. If you need custom error handling, implement it directly in the `main` slot of `<primer-checkout>` without using `<primer-main>`.
      </Info>
    </Accordion>
  </Tab>

  <Tab title="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:

    <ComponentTree nodes={ANDROID_LAYOUT_HIERARCHY} showLegend={false} />
  </Tab>

  <Tab title="iOS">
    The checkout layout uses a scope hierarchy. Each scope exposes customization points for the UI it manages:

    <ComponentTree nodes={IOS_LAYOUT_HIERARCHY} showLegend={false} />
  </Tab>
</Tabs>

## Customization approaches

<Tabs>
  <Tab title="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
    </Tip>

    ### 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](/checkout/primer-checkout/build-your-ui/advanced-layout-customization).
  </Tab>

  <Tab title="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, onEvent = { /* ... */ }) {
        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>

  <Tab title="iOS">
    ### Field-level customization

    Override individual field labels, placeholders, and styling:

    ```swift theme={"dark"}
    cardScope.cardNumberConfig = InputFieldConfig(
      label: "Card number",
      placeholder: "0000 0000 0000 0000"
    )

    cardScope.cvvConfig = InputFieldConfig(
      styling: PrimerFieldStyling(cornerRadius: 12, borderColor: .gray)
    )
    ```

    ### Section-level customization

    Replace entire sections with custom SwiftUI views:

    ```swift theme={"dark"}
    checkoutScope.splashScreen = {
      AnyView(
        VStack {
          ProgressView()
          Text("Loading payment methods...")
        }
      )
    }
    ```

    ### Screen-level customization

    Replace full screens while keeping scope access for state and actions:

    ```swift theme={"dark"}
    if let cardScope: PrimerCardFormScope = checkoutScope.getPaymentMethodScope(PrimerCardFormScope.self) {
      cardScope.screen = { scope in
        AnyView(MyCustomCardFormScreen(scope: scope))
      }
    }
    ```
  </Tab>
</Tabs>

## Why customization points matter

<Tabs>
  <Tab title="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>

  <Tab title="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>

  <Tab title="iOS">
    Scope-based customization provides similar benefits to Web slots and Android slot parameters:

    1. **Component targeting** - Each scope property maps to a specific part of the UI
    2. **Default content** - Any scope property you don't override uses the built-in default
    3. **Type safety** - Closures receive typed scope parameters, giving you access to state and actions

    When replacing a screen, your closure receives the relevant scope object, ensuring you have access to everything needed to build a functional replacement.
  </Tab>
</Tabs>

## Styling custom layouts

<Tabs>
  <Tab title="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](/sdk/primer-checkout-web/components/primer-checkout)
    * [Main Component](/sdk/primer-checkout-web/components/primer-main)
    * [Payment Method Component](/sdk/primer-checkout-web/components/primer-payment-method)
  </Tab>

  <Tab title="Android">
    Use `PrimerTheme` design tokens to maintain visual consistency with checkout components. See [Styling Customization](/checkout/primer-checkout/build-your-ui/styling-customization) for details.
  </Tab>

  <Tab title="iOS">
    Use `PrimerCheckoutTheme` design tokens to maintain visual consistency with checkout components. Use `PrimerFieldStyling` for per-field styling overrides. See [Styling Customization](/checkout/primer-checkout/build-your-ui/styling-customization) for details.
  </Tab>
</Tabs>

## 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 style={{ verticalAlign: 'middle' }}><strong>Custom sectioned layout</strong></td>
      <td style={{ verticalAlign: 'middle' }}>Payment methods organized into sections (Card, Quick Checkout, Alternative Methods)</td>

      <td style={{ verticalAlign: 'middle' }}>
        <a href="https://stackblitz.com/fork/github/primer-io/examples/tree/main/examples/primer-checkout-custom-layout" target="_blank">
          <img src="https://developer.stackblitz.com/img/open_in_stackblitz.svg" alt="Open in StackBlitz" noZoom />
        </a>
      </td>
    </tr>

    <tr>
      <td style={{ verticalAlign: 'middle' }}><strong>Custom form layout</strong></td>
      <td style={{ verticalAlign: 'middle' }}>Reordered card form components with custom styling and layout</td>

      <td style={{ verticalAlign: 'middle' }}>
        <a href="https://stackblitz.com/fork/github/primer-io/examples/tree/main/examples/primer-checkout-custom-form" target="_blank">
          <img src="https://developer.stackblitz.com/img/open_in_stackblitz.svg" alt="Open in StackBlitz" noZoom />
        </a>
      </td>
    </tr>
  </tbody>
</table>

## See also

<CardGroup cols={2}>
  <Card title="Slot architecture explorer" icon="table-layout" href="/checkout/primer-checkout/build-your-ui/slot-architecture-explorer">
    Explore the full component hierarchy and all available slots interactively
  </Card>

  <Card title="Layout with event handling" icon="bolt" href="/checkout/primer-checkout/build-your-ui/advanced-layout-customization">
    Learn about events, dynamic rendering, and fully custom implementations
  </Card>

  <Card title="Error handling" icon="triangle-exclamation" href="/checkout/primer-checkout/build-your-ui/error-handling">
    Handle payment failures and display error messages
  </Card>

  <Card title="Design tokens explorer" icon="droplet" href="/checkout/primer-checkout/build-your-ui/design-tokens-explorer">
    Explore CSS variables and see which components they affect
  </Card>
</CardGroup>
