Quick diagnosis
| Symptom | Likely Cause | Solution |
|---|
| Components don’t render | loadPrimer() not called | Call loadPrimer() before using components |
customElements is not defined | Code running on server | Load SDK only on client side |
window is not defined | Code running on server | Add typeof window !== 'undefined' check |
| Two card forms appear | Using both custom form and payment method | Choose one approach |
| Card inputs don’t work | Inputs outside <primer-card-form> | Wrap inputs in card form component |
| Options not applied (React) | New object reference each render | Use stable references with useMemo or constants |
| Styles not applying | Trying to style shadow DOM | Use 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.
Next.js / React
Nuxt 3
SvelteKit
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>
);
}
<script setup>
import { onMounted } from 'vue';
onMounted(async () => {
if (import.meta.client) {
const { loadPrimer } = await import('@primer-io/primer-js');
loadPrimer();
}
});
</script>
<script>
import { onMount } from 'svelte';
import { browser } from '$app/environment';
onMount(async () => {
if (browser) {
const { loadPrimer } = await import('@primer-io/primer-js');
loadPrimer();
}
});
</script>
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
}
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>
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} />;
}
const SDK_OPTIONS = { locale: 'en-GB' };
function CheckoutPage() {
return <primer-checkout client-token={token} options={SDK_OPTIONS} />;
}
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 Type | When It Occurs | How It’s Handled |
|---|
| Validation errors | During input (invalid format, missing fields) | Handled automatically by input components; prevents form submission |
| Payment failures | After 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:
- The
diagnosticsId from any error callbacks
- Your browser and version
- Your framework and version
- 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