Skip to main content
Primer Checkout requires browser APIs and must be loaded on the client side only. This guide shows the patterns for each SSR framework.

Quick Fix

The core pattern is the same for all frameworks—load Primer inside a client-side lifecycle hook:
// Load Primer only on the client
useEffect(() => {
  loadPrimer();
}, []);
That’s it. The sections below show this pattern for each framework.

Next.js

'use client';

import { useEffect } from 'react';
import { loadPrimer } from '@primer-io/primer-js';

const SDK_OPTIONS = { locale: 'en-GB' };

export default function CheckoutPage() {
  useEffect(() => {
    loadPrimer();
  }, []);

  return (
    <primer-checkout
      client-token="your-client-token"
      options={SDK_OPTIONS}
    />
  );
}
The 'use client' directive marks the component as client-side only.

Pages Router

import { useEffect } from 'react';
import { loadPrimer } from '@primer-io/primer-js';

const SDK_OPTIONS = { locale: 'en-GB' };

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

  return (
    <primer-checkout
      client-token="your-client-token"
      options={SDK_OPTIONS}
    />
  );
}

Using next/dynamic (Alternative)

Disable SSR for the entire checkout component:
// components/Checkout.tsx
'use client';

import { useEffect } from 'react';
import { loadPrimer } from '@primer-io/primer-js';

const SDK_OPTIONS = { locale: 'en-GB' };

export function Checkout({ clientToken }: { clientToken: string }) {
  useEffect(() => {
    loadPrimer();
  }, []);

  return (
    <primer-checkout
      client-token={clientToken}
      options={SDK_OPTIONS}
    />
  );
}
// pages/checkout.tsx or app/checkout/page.tsx
import dynamic from 'next/dynamic';

const Checkout = dynamic(
  () => import('@/components/Checkout').then((mod) => mod.Checkout),
  { 
    ssr: false,
    loading: () => <div>Loading checkout...</div>
  }
);

export default function CheckoutPage() {
  return <Checkout clientToken="your-client-token" />;
}
For React 18 vs 19 patterns and handling payment events, see the React Integration Guide.

Nuxt.js

Nuxt 3

<template>
  <div>
    <primer-checkout client-token="your-client-token" />
  </div>
</template>

<script setup>
import { onMounted } from 'vue';

onMounted(async () => {
  if (import.meta.client) {
    const { loadPrimer } = await import('@primer-io/primer-js');
    loadPrimer();
  }
});
</script>
Use import.meta.client in Nuxt 3. The older process.client still works but is a legacy pattern.
<template>
  <div>
    <primer-checkout client-token="your-client-token" />
  </div>
</template>

<script>
export default {
  mounted() {
    if (process.client) {
      import('@primer-io/primer-js')
        .then(({ loadPrimer }) => {
          loadPrimer();
        })
        .catch((error) => {
          console.error('Failed to load Primer:', error);
        });
    }
  }
}
</script>

SvelteKit

<script>
  import { onMount } from 'svelte';
  import { browser } from '$app/environment';

  onMount(async () => {
    if (browser) {
      const { loadPrimer } = await import('@primer-io/primer-js');
      loadPrimer();
    }
  });
</script>

<primer-checkout client-token="your-client-token" />

Show Loading State

Wait for Primer to load before showing the checkout:
'use client';

import { useEffect, useState } from 'react';
import { loadPrimer } from '@primer-io/primer-js';

export default function CheckoutPage() {
  const [isLoaded, setIsLoaded] = useState(false);

  useEffect(() => {
    loadPrimer().then(() => setIsLoaded(true));
  }, []);

  if (!isLoaded) {
    return <div>Loading payment options...</div>;
  }

  return <primer-checkout client-token="your-client-token" />;
}

Troubleshooting

”customElements is not defined”

Primer code is running on the server. Fix: Move loadPrimer() inside a client-side lifecycle hook (useEffect, onMounted, onMount).

“window is not defined”

Code is accessing browser globals during SSR. Fix: Add an environment check:
if (typeof window !== 'undefined') {
  // Browser-only code
}

Components Don’t Render

loadPrimer() hasn’t completed before the component renders. Fix: Use a loading state to defer rendering until Primer is ready. See Show Loading State above.

TypeScript Errors

TypeScript doesn’t recognize <primer-checkout> as a valid element. Fix: Add type declarations:
// src/types/primer.d.ts
import { CustomElements } from '@primer-io/primer-js/dist/jsx/index';

declare module 'react' {
  namespace JSX {
    interface IntrinsicElements extends CustomElements {}
  }
}

Why SSR Needs This

Server-side rendering frameworks execute code on the server to generate HTML before sending it to the browser. Primer Checkout depends on browser-only APIs:
  • Web Components APIcustomElements.define() only exists in browsers
  • DOM APIs – Component rendering requires the Document Object Model
  • Window Object – Payment features depend on window and browser globals
  • Secure Iframes – Card inputs require a browser environment
When SSR frameworks try to execute Primer code on the server, they encounter errors because these APIs don’t exist in Node.js. The solution is to ensure Primer only loads on the client side using lifecycle hooks that run after hydration.

Next Steps