Skip to main content

Quick diagnosis

SymptomLikely CauseSolution
Components don’t renderloadPrimer() not calledCall loadPrimer() before using components
customElements is not definedCode running on serverLoad SDK only on client side
window is not definedCode running on serverAdd typeof window !== 'undefined' check
Two card forms appearUsing both custom form and payment methodChoose one approach
Card inputs don’t workInputs outside <primer-card-form>Wrap inputs in card form component
Options not applied (React)New object reference each renderUse stable references with useMemo or constants
Styles not applyingTrying to style shadow DOMUse CSS variables instead

Server-side rendering errors

Error: “customElements is not defined”

Cause: Primer code is running on the server where Web Components API doesn’t exist. Solution: Ensure loadPrimer() is called only in client-side lifecycle methods.
import { useEffect } from 'react';
import { loadPrimer } from '@primer-io/primer-js';

function MyCheckoutComponent() {
  useEffect(() => {
    if (typeof window !== 'undefined') {
      loadPrimer();
    }
  }, []);

  return (
    <primer-checkout client-token="your-client-token">
      {/* Checkout content */}
    </primer-checkout>
  );
}

Error: “window is not defined”

Cause: Code is accessing browser globals during server-side rendering. Solution: Add environment checks before accessing browser APIs:
if (typeof window !== 'undefined') {
  // Browser-only code here
}

Card form issues

Duplicate card forms

Cause: Using both <primer-card-form> and <primer-payment-method type="PAYMENT_CARD"> in the same layout. Why this happens: <primer-payment-method type="PAYMENT_CARD"> internally creates its own <primer-card-form>. When you also add a custom card form, you end up with two card forms on the page. Solution: Choose one approach:
<!-- Option A: Use custom card form only -->
<div slot="payments">
  <primer-card-form>
    <div slot="card-form-content">
      <!-- Your custom layout -->
    </div>
  </primer-card-form>
  <!-- Other payment methods, but NOT PAYMENT_CARD -->
  <primer-payment-method type="PAYPAL"></primer-payment-method>
</div>

<!-- Option B: Use payment method component only -->
<div slot="payments">
  <!-- Let the component handle the card form -->
  <primer-payment-method type="PAYMENT_CARD"></primer-payment-method>
  <primer-payment-method type="PAYPAL"></primer-payment-method>
</div>

Card inputs not working

Cause: Card input components placed outside <primer-card-form>. Solution: All card inputs must be descendants of the card form:
<!-- WRONG: Inputs outside primer-card-form -->
<primer-card-form></primer-card-form>
<primer-input-card-number></primer-input-card-number>

<!-- CORRECT: Inputs inside primer-card-form -->
<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>
  </div>
</primer-card-form>

Dynamic rendering creates duplicates

Cause: When dynamically rendering payment methods, PAYMENT_CARD is included while also using a custom card form. Solution: Filter out PAYMENT_CARD when 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');

  availableMethods.forEach((method) => {
    const element = document.createElement('primer-payment-method');
    element.setAttribute('type', method.type);
    container.appendChild(element);
  });
});

React-specific issues

Options not being applied

Cause: Creating new object references on every render forces unnecessary comparisons.
The Primer SDK implements deep comparison for the options property. This means unstable references won’t cause re-initialization, but they still add comparison overhead on every render.
Solution: Use stable references:
// SUBOPTIMAL: New object every render
function CheckoutPage() {
  return <primer-checkout options={{ locale: 'en-GB' }} />;
}

// OPTIMAL: Stable reference
const SDK_OPTIONS = { locale: 'en-GB' };

function CheckoutPage() {
  return <primer-checkout options={SDK_OPTIONS} />;
}

// OPTIMAL: Use useMemo for dynamic options
function CheckoutPage({ userLocale }) {
  const options = useMemo(() => ({
    locale: userLocale,
  }), [userLocale]);

  return <primer-checkout options={options} />;
}

Calling methods before ready

Cause: Calling SDK methods before the checkout is initialized. Solution: Wait for the primer:ready event:
// WRONG: May fail if checkout isn't ready
const checkout = document.querySelector('primer-checkout');
const primerJS = checkout.primerJS; // May be undefined
primerJS.setCardholderName('John Doe'); // Error!

// CORRECT: Wait for the ready event
checkout.addEventListener('primer:ready', (event) => {
  const primerJS = event.detail;
  primerJS.setCardholderName('John Doe'); // Works correctly
});

React 18 vs React 19 property assignment

Cause: React 18 converts object props to [object Object] strings for web components. Solution: Use the appropriate pattern for your React version:
const SDK_OPTIONS = { locale: 'en-GB' };

function CheckoutPage() {
  const checkoutRef = useRef(null);

  useEffect(() => {
    if (checkoutRef.current) {
      checkoutRef.current.options = SDK_OPTIONS;
    }
  }, []);

  return <primer-checkout ref={checkoutRef} client-token={token} />;
}

Styling issues

CSS not applying to components

Cause: Trying to style internal elements with CSS selectors. Shadow DOM prevents external CSS from reaching internal elements. Solution: Use CSS variables instead:
/* WRONG: Won't work with Shadow DOM */
primer-checkout input {
  border-color: blue;
}

/* CORRECT: Use CSS variables */
primer-checkout {
  --primer-color-border-input-default: blue;
}

Validation vs payment errors

Understanding the difference helps with proper error handling:
Error TypeWhen It OccursHow It’s Handled
Validation errorsDuring input (invalid format, missing fields)Handled automatically by input components; prevents form submission
Payment failuresAfter form submission (declined card, network issues)Requires explicit handling with error container or custom code
Don’t confuse these two error types. Validation errors prevent form submission and are shown inline. Payment failures occur after the form is submitted and require explicit handling.

Debugging tips

Log all Primer events

const primerEvents = [
  'primer:ready',
  'primer:state-change',
  'primer:methods-update',
  'primer:payment-success',
  'primer:payment-failure',
  'primer:card-error',
  'primer:card-network-change',
];

primerEvents.forEach((eventName) => {
  document.addEventListener(eventName, (event) => {
    console.log(`[${eventName}]`, event.detail);
  });
});

Verify component registration

// Should return a constructor, not undefined
console.log(customElements.get('primer-checkout'));

Check available payment methods

checkout.addEventListener('primer:methods-update', (event) => {
  const methods = event.detail.toArray();
  console.table(methods.map((m) => ({ type: m.type, id: m.id })));
});

Getting help

When contacting Primer support, include:
  1. The diagnosticsId from any error callbacks
  2. Your browser and version
  3. Your framework and version
  4. Steps to reproduce the issue
primer.onPaymentFailure = ({ error }) => {
  console.error('Payment failed:', {
    code: error.code,
    message: error.message,
    diagnosticsId: error.diagnosticsId, // Include this in support requests
  });
};

See also