Card Form Customizations Guide
The Primer SDK's card form components provide a secure way to collect payment card information while offering extensive customization options. This guide explains the fundamental concepts behind card form customization and how to tailor it to match your brand's requirements.
Interactive Card Form Layout Builderβ
Experience the power of drag-and-drop form building! The interactive builder below lets merchants visually design their card form layouts without writing code.
π― Key Features:
- Drag & Drop: Drag components from the palette directly into layout rows
 - Live Preview: See your HTML code update in real-time as you build
 - Component Configuration: Click the gear icon (β) to customize labels, placeholders, and accessibility attributes
 - Reordering: Drag rows vertically and components horizontally to perfect your layout
 - Visual Feedback: Clear visual cues show drag states and drop zones
 
Interactive Card Form Layout Builder
Drag components from the palette into rows, reorder them, and see the generated HTML code
Layout Builder
Component Palette
Generated HTML Code
<primer-card-form>
  <!-- Default layout with all components -->
</primer-card-form>
Card Form Component Architectureβ
The card form uses a component-based architecture that separates concerns while maintaining security and compliance. At the heart of this architecture is the parent-child relationship between components:
Component Relationshipsβ
The card form components have a strict hierarchical relationship:
- Parent Container: 
<primer-card-form>serves as both the container and context provider - Child Components: Input fields and submit button must be descendants of the card form
 - Context Dependency: Child components depend on the context provided by the parent
 
Card Form Componentsβ
Card Form Components
<primer-card-form>- The container that orchestrates validation and submission<primer-input-card-number>- Secure field for card number collection with network detection<primer-input-card-expiry>- Secure field for expiration date collection<primer-input-cvv>- Secure field for security code collection<primer-input-card-holder-name>- Field for cardholder name collection<primer-card-form-submit>- Submit button with contextual styling
Each component is designed to work within the <primer-card-form> container, which provides:
- Context: Validation state, error handling, and form state
 - Event Handling: Form submission and validation events
 - Hosted Inputs: Secure iframe-based input fields for PCI compliance
 - Layout Structure: Default or customizable layout options
 
Understanding Card Form Slot Customizationβ
The card form uses a slot-based customization model to allow flexible layouts without compromising security.
The card-form-content Slotβ
The primary customization point is the card-form-content slot within the <primer-card-form> component.
This slot allows you to:
- Arrange input fields in your preferred order
 - Group fields together (e.g., expiry and CVV in a row)
 - Add custom elements alongside secure inputs
 - Apply your own styling and layout
 
<primer-card-form>
  <div slot="card-form-content">
    <!-- Your custom layout here -->
  </div>
</primer-card-form>
When you don't provide content for this slot, the card form automatically renders a default layout with all required fields.
Component Dependency and Contextβ
All card form input components have a critical relationship with the parent card form.
This relationship means:
- Mandatory Containment: Card input components must always be placed inside a 
<primer-card-form>component - Context Access: Components access secure hosted inputs, validation state, and form management through the parent's context
 - Event Bubbling: Events from child components bubble up to the parent for processing
 - Coordinated Validation: The parent coordinates validation across all input components
 
The <primer-billing-address> component can be included in the card-form-content slot to collect billing information. This component is available when SDK Core is enabled (default) and can be configured for drop-in or custom layout modes. See the Billing Address Component for detailed configuration options and styling.
Customizing Input Field Appearanceβ
Each card input component accepts properties that modify its appearance without affecting functionality:
Label and Placeholder Customizationβ
You can customize the visible text for each input:
<primer-input-card-number
  label="Card Number"           <!-- Changes the label text -->
placeholder="1234 5678 9012 3456" <!-- Changes the placeholder text -->
aria-label="Credit card number" <!-- Changes the accessibility label -->
></primer-input-card-number>
These properties work consistently across all card input components, allowing for uniform customization.
Form Layout Patternsβ
While you have complete freedom over the layout, certain patterns are common and effective:
Standard Vertical Layoutβ
The most common pattern is a vertical stack of inputs:
<primer-card-form>
  <div slot="card-form-content">
    <primer-input-card-number></primer-input-card-number>
    <primer-input-card-holder-name></primer-input-card-holder-name>
    <div style="display: flex; gap: 8px;">
      <primer-input-card-expiry></primer-input-card-expiry>
      <primer-input-cvv></primer-input-cvv>
    </div>
    <primer-card-form-submit></primer-card-form-submit>
  </div>
</primer-card-form>
This pattern places related fields (expiry and CVV) side-by-side while keeping the main inputs full-width.
How Form Submission Worksβ
The <primer-card-form> component handles form submission in several ways:
Submission Trigger Methods
- Through the 
<primer-card-form-submit>component (recommended) - Through any HTML button with 
type="submit" - Through any element with the 
data-submitattribute 
When submission is triggered, the component:
- Validates all card inputs
 - Emits validation errors if necessary
 - Submits the payment if validation passes
 - Emits success or error events based on the outcome
 
Event-Driven Integrationβ
The card form follows an event-driven approach for both validation handling and programmatic control. This section covers how to listen for events and trigger actions programmatically.
For comprehensive information on all available events, event payloads, and best practices, see the Events Guide.
Selecting Card Form Componentsβ
To interact with card form components programmatically, you need to select them from the DOM:
// Select the main checkout component
const checkout = document.querySelector('primer-checkout');
// Select a specific card form (if using custom layout)
const cardForm = document.querySelector('primer-card-form');
Components must be present in the DOM before you can interact with them. Ensure your code runs after the components are loaded and rendered in the UI.
Event Listeningβ
const checkout = document.querySelector('primer-checkout');
// Listen for validation errors
checkout.addEventListener('primer:card-error', (event) => {
  const errors = event.detail.errors;
  // Handle validation errors
});
// Listen for successful submission
checkout.addEventListener('primer:card-success', (event) => {
  const result = event.detail.result;
  // Handle successful submission
});
// Listen for card network changes
checkout.addEventListener('primer:card-network-change', (event) => {
  const network = event.detail;
  // Handle card network detection/change
});
These events bubble up to the <primer-checkout> component, allowing you to handle them at any level.
Programmatic Form Submissionβ
You can trigger card form submission programmatically by dispatching a primer:card-submit event to the document:
// Trigger card form submission from anywhere in your application
document.dispatchEvent(
  new CustomEvent('primer:card-submit', {
    bubbles: true,
    composed: true,
    detail: { source: 'custom-submit-button' },
  }),
);
The checkout component listens for this event at the document level and forwards it internally to the card form, so you don't need to reference the card form element directly.
The checkout component listens for primer:card-submit events at the document level. You can dispatch this event from anywhere in your applicationβthe event will bubble up through the DOM and be captured by the checkout component, which forwards it internally to the card form.
This approach is useful for:
- Custom submit buttons outside the card form
 - Triggering submission from other UI elements
 - Integrating with external form validation systems
 
Complete Example: Custom Submit Button with Event Handling
<primer-checkout client-token="your-client-token">
  <primer-main slot="main">
    <div slot="payments">
      <primer-card-form>
        <div slot="card-form-content">
          <primer-input-card-number></primer-input-card-number>
          <primer-input-card-expiry></primer-input-card-expiry>
          <primer-input-cvv></primer-input-cvv>
          <!-- Custom submit button -->
          <button type="button" id="custom-submit" class="custom-pay-button">
            Pay Now
          </button>
        </div>
      </primer-card-form>
    </div>
  </primer-main>
</primer-checkout>
<script>
  // Set up custom submit button
  document.getElementById('custom-submit').addEventListener('click', () => {
    // Dispatch to document - checkout listens at document level
    document.dispatchEvent(
      new CustomEvent('primer:card-submit', {
        bubbles: true,
        composed: true,
        detail: { source: 'custom-pay-button' },
      }),
    );
  });
  // Handle submission results
  const checkout = document.querySelector('primer-checkout');
  checkout.addEventListener('primer:card-success', (event) => {
    console.log('Payment successful:', event.detail.result);
    // Handle success (e.g., redirect to confirmation page)
  });
  checkout.addEventListener('primer:card-error', (event) => {
    console.log('Validation errors:', event.detail.errors);
    // Handle errors (e.g., show error messages)
  });
</script>
Styling Card Form Componentsβ
Card form components inherit styling from CSS custom properties defined at the checkout level:
:root {
  /* These properties affect all components */
  --primer-color-brand: #4a90e2;
  --primer-radius-small: 4px;
  --primer-typography-body-large-font: 'Your-Font', sans-serif;
}
Integrating Custom Fieldsβ
You can seamlessly integrate custom fields alongside the secure card inputs:
<primer-card-form>
  <div slot="card-form-content">
    <primer-input-card-number></primer-input-card-number>
    <!-- Primer billing address component (optional) -->
    <primer-billing-address></primer-billing-address>
    <!-- Custom field using primer-input -->
    <primer-input-wrapper>
      <primer-input-label slot="label">Billing Zip Code</primer-input-label>
      <primer-input slot="input" type="text" name="zip"></primer-input>
    </primer-input-wrapper>
    <div style="display: flex; gap: 8px;">
      <primer-input-card-expiry></primer-input-card-expiry>
      <primer-input-cvv></primer-input-cvv>
    </div>
    <primer-card-form-submit></primer-card-form-submit>
  </div>
</primer-card-form>
The form container doesn't validate these custom fields directly, so you'll need to implement your own validation if needed. The <primer-billing-address> component handles its own validation internally.
Avoiding Duplicate Card Form Renderingβ
When customizing your card form layout, be aware of a common issue that can lead to duplicate card form elements:
<!-- β INCORRECT: This will cause duplicate card forms to appear -->
<primer-checkout client-token="your-token">
  <primer-main slot="main">
    <div slot="payments">
      <!-- Custom card form -->
      <primer-card-form>
        <div slot="card-form-content">
          <!-- Card form inputs -->
        </div>
      </primer-card-form>
      <!-- This will render ANOTHER card form, causing duplicates -->
      <primer-payment-method type="PAYMENT_CARD"></primer-payment-method>
    </div>
  </primer-main>
</primer-checkout>
Important: When using a custom card form, do not include <primer-payment-method type="PAYMENT_CARD"> in your layout. The payment method component will render its own card form, resulting in duplicates.
This is especially important when dynamically generating payment methods:
Dynamic Payment Method Filtering Example
// When dynamically rendering payment methods, filter out PAYMENT_CARD if you're using a custom card form
checkout.addEventListener('primer:methods-update', (event) => {
  const availableMethods = event.detail
    .toArray()
    // Filter out PAYMENT_CARD if you're using a custom card form
    .filter((method) => method.type !== 'PAYMENT_CARD');
  // Render the filtered payment methods
  availableMethods.forEach((method) => {
    const element = document.createElement('primer-payment-method');
    element.setAttribute('type', method.type);
    container.appendChild(element);
  });
});
Relationship with Payment Method Componentβ
The relationship between <primer-card-form> and <primer-payment-method type="PAYMENT_CARD"> is important to understand:
Key points about this relationship:
<primer-payment-method type="PAYMENT_CARD">internally creates its own<primer-card-form>- You should use either:
- A custom 
<primer-card-form>(for full layout control) - The 
<primer-payment-method type="PAYMENT_CARD">component (for automatic handling) 
 - A custom 
 - Using both simultaneously will create duplicate forms and cause conflicts
 
Best Practicesβ
- Maintain Security - Always use the provided secure input components for card data
 - Respect Component Hierarchy - Keep all card input components within the 
primer-card-form - Avoid Duplicate Components - Don't use 
<primer-payment-method type="PAYMENT_CARD">with a custom card form - Prioritize Clarity - Keep layouts simple and focused on the payment task
 - Use Consistent Styling - Maintain visual consistency with your site's design system
 - Handle Validation Properly - Provide clear error messages and guidance
 - Consider Mobile First - Design for small screens first, then enhance for larger devices
 - Test Thoroughly - Validate behavior across browsers and device types
 
For detailed information on individual components, refer to their SDK Reference documentation: