Layout Customizations Guide
Primer Checkout provides a flexible, slot-based architecture that allows you to customize the checkout layout while maintaining the core payment functionality. This guide explains the fundamental concepts behind layout customization and how to implement your own checkout experience.
Understanding Slot-Based Architecture
Primer Checkout uses slots as the primary mechanism for layout customization. Slots are named placeholders in components where you can insert your own content.
What Are Slots?
Slots are designated areas within Web Components where custom content can be inserted. Each slot has a specific name that determines where the content will appear.
<!-- This content will be inserted into the "main" slot of primer-checkout -->
<div slot="main">Your custom content goes here</div>
When a component renders, it replaces each slot with the content you provide. If you don't provide content for a slot, the component often uses default content instead.
Slots allow you to customize specific parts of a component without having to recreate the entire component's functionality.
Component Hierarchy and Available Slots
The checkout layout follows a hierarchical structure with slots at each level:
1. <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
<primer-checkout client-token="your-token">
  <div slot="main">
    <!-- Your custom checkout UI -->
  </div>
</primer-checkout>
2. <primer-main> Component (Optional)
A pre-built component that manages checkout states and provides additional slots for customization.
Available Slots:
payments- Contains payment method componentscheckout-complete- Content shown on successful paymentcheckout-failure` - Content shown when payment fails
<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-failure">
      <!-- Your error screen -->
    </div>
  </primer-main>
</primer-checkout>
Customization Approaches
You can customize the checkout layout in two main ways:
Approach 1: 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:
<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>
<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
 
Approach 2: Fully Custom Implementation
For complete control, you can bypass <primer-main> entirely and provide your own implementation:
<primer-checkout client-token="your-token">
  <div slot="main" id="custom-checkout">
    <!-- Your completely custom checkout implementation -->
    <div id="payment-methods">
      <primer-payment-method type="PAYMENT_CARD"></primer-payment-method>
      <!-- Option 1: Use the pre-built error container component for payment failures -->
      <primer-error-message-container></primer-error-message-container>
      <!-- Option 2: Or create your own error display element -->
      <div id="my-custom-error" class="custom-error-message"></div>
    </div>
  </div>
</primer-checkout>
When using this approach:
- You must handle state management yourself through events
 - You have complete freedom over the layout and user flow
 - You're responsible for showing/hiding appropriate content based on checkout state
 - You need to handle payment failure display, either with the 
<primer-error-message-container>component or by implementing custom error handling with events 
Why Slot Names Matter
Slot names are crucial for several reasons:
- Component Targeting - Names tell the component exactly where to insert your content
 - Default Content - Components can provide default content for slots that aren't filled
 - Preventing Accidental Rendering - Content without a matching slot won't be displayed
 - 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.
Event-Driven State Management
When implementing a custom layout without <primer-main>, you'll need to listen for events to manage checkout states.
For comprehensive information on all available events, event payloads, and best practices, see the Events Guide.
document
  .querySelector('primer-checkout')
  .addEventListener('primer:state-change', (event) => {
    const state = event.detail;
    // Handle different checkout states
    if (state.isProcessing) {
      // Show loading indicator
    } else if (state.isSuccessful) {
      // Show success message
    } else if (state.error) {
      // Show error message
    }
  });
Key events to listen for
primer:state-change- Fired when checkout state changesprimer:methods-update- Fired when available payment methods are loadedprimer:ready- Fired when the SDK is ready
Configuring Payment Methods
When customizing the payment method layout, you can include specific payment methods:
<div slot="payments">
  <primer-payment-method type="PAYMENT_CARD"></primer-payment-method>
  <primer-payment-method type="PAYPAL"></primer-payment-method>
</div>
The type attribute specifies which payment method to display. If a payment method isn't available in your configuration, it simply won't render.
Important: Avoiding Duplicate Card Forms
When customizing your checkout layout, be careful not to render duplicate card forms. This commonly happens when:
- You create a custom card form using 
<primer-card-form> - You also include 
<primer-payment-method type="PAYMENT_CARD">in your layout 
<!-- ❌ INCORRECT: Will result in duplicate card forms -->
<div slot="payments">
  <!-- Custom card form -->
  <primer-card-form>
    <!-- Custom card form content -->
  </primer-card-form>
  <!-- This will render ANOTHER card form -->
  <primer-payment-method type="PAYMENT_CARD"></primer-payment-method>
</div>
If you're using a custom card form implementation, you should not include the PAYMENT_CARD payment method in your layout.
Declarative Payment Method Filtering (Recommended)
The primer-payment-method-container component provides a declarative way to organize payment methods:
<!-- Sectioned layout example -->
<div slot="payments">
  <!-- Quick pay options -->
  <primer-payment-method-container
    include="APPLE_PAY,GOOGLE_PAY"
  ></primer-payment-method-container>
  <!-- Alternative methods -->
  <primer-payment-method-container
    exclude="PAYMENT_CARD,APPLE_PAY,GOOGLE_PAY"
  ></primer-payment-method-container>
  <!-- Card form -->
  <primer-payment-method type="PAYMENT_CARD"></primer-payment-method>
</div>
This approach automatically filters available payment methods without requiring event listeners or manual state management. See the Payment Method Container SDK Reference documentation for complete usage guide.
Alternative: Event-Driven Dynamic Rendering
You can also dynamically render payment methods by listening to the primer:methods-update event:
Example: Dynamic Payment Method Rendering
checkout.addEventListener('primer:methods-update', (event) => {
  const availableMethods = event.detail.toArray();
  const container = document.getElementById('payment-methods');
  // Create payment method elements based on available methods
  availableMethods.forEach((method) => {
    const element = document.createElement('primer-payment-method');
    element.setAttribute('type', method.type);
    container.appendChild(element);
  });
});
This approach ensures you only display payment methods that are actually available.
Filtering to avoid duplicate card forms
Important: If you're using a custom card form, you should filter out the PAYMENT_CARD type to avoid duplicate card forms:
checkout.addEventListener('primer:methods-update', (event) => {
  let availableMethods = event.detail.toArray();
  const container = document.getElementById('payment-methods');
  // If using a custom card form, filter out PAYMENT_CARD
  if (document.querySelector('primer-card-form')) {
    availableMethods = availableMethods.filter(
      (method) => method.type !== 'PAYMENT_CARD',
    );
  }
  // Create payment method elements based on filtered methods
  availableMethods.forEach((method) => {
    const element = document.createElement('primer-payment-method');
    element.setAttribute('type', method.type);
    container.appendChild(element);
  });
});
Styling Custom Layouts
When styling custom layouts, use CSS custom properties for consistency:
.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.
Handling Flash of Undefined Components
When using slot-based customizations, you might encounter a brief "flash" where your custom content appears before the Primer components are fully initialized. This occurs because web components are registered with JavaScript, which may load after your HTML is rendered.
This is particularly noticeable when:
- You've added custom UI in slots
 - The page loads and shows your custom content
 - The components initialize and potentially hide or rearrange your content
 
Solution: Hide components until ready
To prevent this jarring visual experience, you can hide the components until they're fully defined:
primer-checkout:has(:not(:defined)) {
  visibility: hidden;
}
This CSS rule hides the checkout container when it contains any undefined custom elements. Once all components are defined, the container becomes visible automatically. Using visibility: hidden instead of display: none preserves the layout space to minimize shifting when components appear.
For more complex implementations, you could also use JavaScript to detect when all components are ready:
Promise.allSettled([
  customElements.whenDefined('primer-checkout'),
  customElements.whenDefined('primer-payment-method'),
  // Add other components you're using
]).then(() => {
  document.querySelector('.checkout-container').classList.add('ready');
});
Handling Payment Failure Messages in Custom Layouts
When implementing custom layouts, you have two options for displaying payment failure messages:
- Use the pre-built 
<primer-error-message-container>component - Implement custom error handling using the SDK events
 
Option 1: Using the Error Message Container Component
The <primer-error-message-container> provides a convenient way to display payment failures without writing custom code:
<!-- Approach 1: Using primer-main with custom content -->
<primer-checkout client-token="your-token">
  <primer-main slot="main">
    <div slot="payments">
      <!-- Your payment methods -->
      <primer-payment-method type="PAYMENT_CARD"></primer-payment-method>
      <!-- Include error message container for payment failure display -->
      <primer-error-message-container></primer-error-message-container>
    </div>
  </primer-main>
</primer-checkout>
<!-- Approach 2: Fully custom implementation -->
<primer-checkout client-token="your-token">
  <div slot="main" id="custom-checkout">
    <!-- Your completely custom checkout implementation -->
    <div id="payment-methods">
      <primer-payment-method type="PAYMENT_CARD"></primer-payment-method>
      <!-- Include error message container for payment failure messages -->
      <primer-error-message-container></primer-error-message-container>
    </div>
  </div>
</primer-checkout>
If using the error message container, for optimal user experience, place it:
- Prominently where it will be visible after a payment attempt
 - Where users will naturally look for feedback after submitting payment
 - Within the same visual context as the payment method it relates to
 
Option 2: Custom Payment Failure Handling
You can also implement your own payment failure handling using the SDK events and the new PrimerJS API:
const checkout = document.querySelector('primer-checkout');
// Listen for the checkout ready event
checkout.addEventListener('primer:ready', ({ detail: primer }) => {
  // Set up the payment complete callback
  primer.onPaymentComplete = ({ payment, status, error }) => {
    if (status === 'error') {
      // Display the payment failure using your own UI
      const customErrorElement = document.getElementById('my-custom-error');
      customErrorElement.textContent = error.message;
      customErrorElement.style.display = 'block';
    } else {
      // Hide error element for success/pending states
      document.getElementById('my-custom-error').style.display = 'none';
    }
  };
});
// Option 2: Listen for checkout state changes
checkout.addEventListener('primer:state-change', (event) => {
  const { error, failure } = event.detail;
  if (error || failure) {
    // Display the failure using your own UI
    const customErrorElement = document.getElementById('my-custom-error');
    customErrorElement.textContent = failure ? failure.message : error.message;
    customErrorElement.style.display = 'block';
  } else {
    // Hide error when not present
    document.getElementById('my-custom-error').style.display = 'none';
  }
});
This approach gives you complete control over payment failure presentation but requires you to implement the error handling logic yourself.
The <primer-error-message-container> specifically handles payment failures that occur after form submission, not card validation errors. Card validation is handled by the input components themselves and prevents form submission until valid.
For more information about the error message container, see the Error Message Container Component documentation.
Best Practices for Layout Customization
- Use Named Slots Correctly - Always use the correct slot names to ensure content appears where expected
 - Listen for Relevant Events - Handle checkout state through event listeners
 - Maintain Visual Consistency - Use CSS custom properties for styling
 - Design Responsively - Ensure your layout works on all device sizes
 - Test Thoroughly - Validate behavior across different payment methods and scenarios
 - Prevent Component Flash - Use CSS or JavaScript techniques to hide content until components are defined
 - Handle Payment Failures - Either use the 
<primer-error-message-container>component or implement custom payment failure handling using events 
For detailed information on available components and their slots, refer to the component SDK Reference documentation: