> ## Documentation Index
> Fetch the complete documentation index at: https://primer.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Server-Side Rendering Guide

> Integrate Primer Checkout with Next.js, Nuxt.js, and SvelteKit

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:

```javascript theme={"dark"}
// Load Primer only on the client
useEffect(() => {
  loadPrimer();
}, []);
```

That's it. The sections below show this pattern for each framework.

## Next.js

### App Router (Recommended)

```tsx theme={"dark"}
'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

```tsx theme={"dark"}
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:

```tsx theme={"dark"}
// 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}
    />
  );
}
```

```tsx theme={"dark"}
// 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" />;
}
```

<Info>
  For React 18 vs 19 patterns and handling payment events, see the [React Integration Guide](/checkout/primer-checkout/frameworks/react-integration-guide).
</Info>

## Nuxt.js

### Nuxt 3

```vue theme={"dark"}
<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>
```

<Note>
  Use `import.meta.client` in Nuxt 3. The older `process.client` still works but is a legacy pattern.
</Note>

<Accordion title="Nuxt 2 Pattern">
  ```vue theme={"dark"}
  <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>
  ```
</Accordion>

## SvelteKit

```svelte theme={"dark"}
<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

Use the `primer:ready` event to detect when the checkout is fully initialized:

<Tabs>
  <Tab title="Next.js">
    ```tsx theme={"dark"}
    'use client';

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

    export default function CheckoutPage() {
      const [isReady, setIsReady] = useState(false);
      const checkoutRef = useRef<PrimerCheckoutComponent>(null);

      useEffect(() => {
        loadPrimer();

        const checkout = checkoutRef.current;
        if (!checkout) return;

        const handleReady = () => setIsReady(true);
        checkout.addEventListener('primer:ready', handleReady);

        return () => checkout.removeEventListener('primer:ready', handleReady);
      }, []);

      return (
        <div>
          {!isReady && <div>Loading payment options...</div>}
          <primer-checkout
            ref={checkoutRef}
            client-token="your-client-token"
            style={{ display: isReady ? 'block' : 'none' }}
          />
        </div>
      );
    }
    ```
  </Tab>

  <Tab title="Nuxt 3">
    ```vue theme={"dark"}
    <template>
      <div>
        <div v-if="!isReady">Loading payment options...</div>
        <primer-checkout
          ref="checkoutRef"
          client-token="your-client-token"
          :style="{ display: isReady ? 'block' : 'none' }"
        />
      </div>
    </template>

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

    const isReady = ref(false);
    const checkoutRef = ref(null);

    const handleReady = () => {
      isReady.value = true;
    };

    onMounted(async () => {
      if (import.meta.client) {
        const { loadPrimer } = await import('@primer-io/primer-js');
        loadPrimer();

        checkoutRef.value?.addEventListener('primer:ready', handleReady);
      }
    });

    onUnmounted(() => {
      checkoutRef.value?.removeEventListener('primer:ready', handleReady);
    });
    </script>
    ```
  </Tab>

  <Tab title="SvelteKit">
    ```svelte theme={"dark"}
    <script>
      import { onMount } from 'svelte';
      import { browser } from '$app/environment';

      let isReady = false;
      let checkoutEl;

      onMount(() => {
        if (browser) {
          import('@primer-io/primer-js').then(({ loadPrimer }) => {
            loadPrimer();
          });

          const handleReady = () => {
            isReady = true;
          };

          checkoutEl?.addEventListener('primer:ready', handleReady);

          return () => checkoutEl?.removeEventListener('primer:ready', handleReady);
        }
      });
    </script>

    <div>
      {#if !isReady}
        <div>Loading payment options...</div>
      {/if}
      <primer-checkout
        bind:this={checkoutEl}
        client-token="your-client-token"
        style:display={isReady ? 'block' : 'none'}
      />
    </div>
    ```
  </Tab>
</Tabs>

<Note>
  The `primer:ready` event fires when the checkout has fully initialized with payment methods. The checkout element must be in the DOM to receive a client token and initialize.
</Note>

## 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:

```javascript theme={"dark"}
if (typeof window !== 'undefined') {
  // Browser-only code
}
```

### Components Don't Render

The checkout needs a valid client token to initialize.

**Fix:** Ensure you're passing a valid `client-token` attribute. Use the `primer:ready` event to detect when initialization is complete. See [Show Loading State](#show-loading-state) above.

### TypeScript Errors

TypeScript doesn't recognize `<primer-checkout>` as a valid element.

**Fix:** Add type declarations:

```typescript theme={"dark"}
// 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

<Accordion title="Technical explanation">
  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 API** – `customElements.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.
</Accordion>

## Next Steps

<CardGroup cols={2}>
  <Card title="React Integration" icon="react" href="/checkout/primer-checkout/frameworks/react-integration-guide">
    React 18/19 patterns and event handling
  </Card>

  <Card title="Styling" icon="palette" href="/checkout/primer-checkout/build-your-ui/styling-customization">
    Customize the appearance
  </Card>

  <Card title="Events Guide" icon="bolt" href="/checkout/primer-checkout/configuration/events">
    Handle payment success and failure
  </Card>

  <Card title="Troubleshooting" icon="wrench" href="/checkout/primer-checkout/troubleshooting">
    More solutions for common issues
  </Card>
</CardGroup>
