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

# Framework-Specific Migration Guide

> Migrate to the new Primer Checkout Web SDK with patterns specific to your JavaScript framework — Vanilla JS, React (18/19), Vue 3, Svelte, and Angular.

Primer Checkout is built with [Lit](https://lit.dev/) Web Components. Each JavaScript framework handles Web Components differently — especially around setting properties, passing objects, and handling events. This guide provides the correct migration patterns for each framework.

<Info>
  This guide is for **Web only**. iOS and Android use native UI frameworks (SwiftUI/UIKit and Jetpack Compose respectively) and do not require framework-specific migration steps. See the [Migration Guide](/checkout/primer-checkout/migration/overview) for platform-specific migration instructions.
</Info>

<Note>
  This guide covers **framework-specific** patterns. For the complete list of Web API and configuration changes (callbacks, payment method options, event payloads, vault methods), see the [Migration Guide](/checkout/primer-checkout/migration/overview).
</Note>

***

## Quick comparison

| Task                  | Vanilla JS                     | React 18               | React 19               | Vue 3                            |
| --------------------- | ------------------------------ | ---------------------- | ---------------------- | -------------------------------- |
| Set `client-token`    | `setAttribute()`               | `client-token={val}`   | `client-token={val}`   | `:client-token="val"`            |
| Set `options` object  | `el.options = obj`             | ref + `useEffect`      | JSX prop directly      | `.options="obj"` or ref          |
| Set `custom-styles`   | `setAttribute()` + JSON        | `custom-styles={json}` | `custom-styles={json}` | `:custom-styles="json"`          |
| Listen to events      | `addEventListener()`           | ref + `useEffect`      | ref + `useEffect`      | `@primer:payment-success` or ref |
| Handle payment result | `primer:payment-success` event | Event in `useEffect`   | Event in `useEffect`   | Event listener                   |

***

## Vanilla JavaScript

Vanilla JS has the most straightforward integration. The `<primer-checkout>` element behaves like any other DOM element.

### Property types

Primer Checkout uses two types of configuration. The distinction matters:

| Type                               | How to set                         | Examples                                           |
| ---------------------------------- | ---------------------------------- | -------------------------------------------------- |
| **Component properties** (strings) | `setAttribute()` or HTML attribute | `client-token`, `custom-styles`, `loader-disabled` |
| **SDK options** (objects)          | Direct property assignment         | `options`                                          |

<Warning>
  Don't mix these up. Component properties must use `setAttribute()`. The `options` object must be assigned directly. Using `setAttribute('options', ...)` will not work.
</Warning>

### Migration example

```javascript theme={"dark"}
import { loadPrimer } from '@primer-io/primer-js';

// Load the SDK
loadPrimer();

const checkout = document.querySelector('primer-checkout');

// ✅ Component properties — use setAttribute()
checkout.setAttribute('client-token', clientToken);
checkout.setAttribute('custom-styles', JSON.stringify({
  primerColorBrand: '#4a6cf7',
}));

// ✅ SDK options — assign directly
checkout.options = {
  locale: 'en-GB',
  card: {
    cardholderName: { required: true, visible: true },
  },
};

// ✅ Payment events
checkout.addEventListener('primer:payment-success', (event) => {
  const { payment, paymentMethodType } = event.detail;
  console.log('Last 4:', payment.last4Digits);
  window.location.href = `/confirmation`;
});

checkout.addEventListener('primer:payment-failure', (event) => {
  const { error } = event.detail;
  console.error('Failed:', error.message, error.diagnosticsId);
});
```

<Info>
  **Direct property assignment also works in vanilla JS.** Because `<primer-checkout>` is a Lit element, you can use `checkout.clientToken = '...'` — Lit's `@property` decorator maps between the JS property and the HTML attribute. However, the SDK documentation uses `setAttribute()` for string properties, and we recommend following that convention for consistency.
</Info>

### Intercepting payments

To run validation before payment creation, listen for `primer:payment-start` and use `preventDefault()`:

```javascript theme={"dark"}
checkout.addEventListener('primer:payment-start', (event) => {
  const { continuePaymentCreation, abortPaymentCreation } = event.detail;

  event.preventDefault(); // Stop automatic continuation

  if (!document.getElementById('terms-checkbox').checked) {
    alert('Please accept the terms');
    abortPaymentCreation();
  } else {
    continuePaymentCreation();
  }
});
```

***

## React

React's handling of Web Components differs significantly between React 18 and React 19, particularly for object properties.

### Why React needs special handling

React assumes all JSX props map to HTML attributes — it has no built-in mechanism to set DOM properties on custom elements. This creates two problems:

1. **CamelCase props are lowercased.** Writing `<primer-checkout clientToken="..." />` sets the attribute `clienttoken` (all lowercase), which the component doesn't recognize.
2. **Objects are stringified.** In React 18, passing an object prop like `options={{ locale: 'en-GB' }}` results in the attribute `options="[object Object]"`.

React 19 added native support for custom element properties, which resolves the object problem but not the attribute naming issue.

| What you write in React JSX                | What React does                            | Result                 |
| ------------------------------------------ | ------------------------------------------ | ---------------------- |
| `clientToken="..."`                        | Sets attribute `clienttoken` (lowercased)  | ❌ Wrong attribute name |
| `client-token="..."`                       | Sets attribute `client-token`              | ✅ Works                |
| `ref.current.clientToken = value`          | Sets JS property directly                  | ✅ Works                |
| `options={{ locale: 'en-GB' }}` (React 18) | Sets attribute `options="[object Object]"` | ❌ Fails                |
| `options={{ locale: 'en-GB' }}` (React 19) | Sets property directly                     | ✅ Works                |

### React 19

React 19 can pass objects directly as properties to custom elements.

```tsx theme={"dark"}
import { useEffect, useRef } from 'react';
import { loadPrimer } from '@primer-io/primer-js';

// Define options outside component for stable reference
const SDK_OPTIONS = { locale: 'en-GB' };

export function Checkout({ clientToken }: { clientToken: string }) {
  const checkoutRef = useRef<PrimerCheckoutComponent>(null);

  useEffect(() => {
    loadPrimer();
  }, []);

  // Set up event handlers
  useEffect(() => {
    const checkout = checkoutRef.current;
    if (!checkout) return;

    const handleSuccess = (event: PrimerEvents['primer:payment-success']) => {
      const { payment } = event.detail;
      window.location.href = `/confirmation`;
    };

    const handleFailure = (event: PrimerEvents['primer:payment-failure']) => {
      const { error } = event.detail;
      console.error('Payment failed:', error.message);
    };

    checkout.addEventListener('primer:payment-success', handleSuccess);
    checkout.addEventListener('primer:payment-failure', handleFailure);

    return () => {
      checkout.removeEventListener('primer:payment-success', handleSuccess);
      checkout.removeEventListener('primer:payment-failure', handleFailure);
    };
  }, []);

  return (
    <primer-checkout
      ref={checkoutRef}
      client-token={clientToken}
      options={SDK_OPTIONS}
    />
  );
}
```

### React 18

React 18 cannot pass objects via JSX props — they become `[object Object]` strings. Use a ref to set the `options` property imperatively.

```tsx theme={"dark"}
import { useEffect, useRef } from 'react';
import { loadPrimer } from '@primer-io/primer-js';

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

export function Checkout({ clientToken }: { clientToken: string }) {
  const checkoutRef = useRef<PrimerCheckoutComponent>(null);

  useEffect(() => {
    loadPrimer();
  }, []);

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

    // React 18: Must set object properties via ref
    checkout.options = SDK_OPTIONS;

    const handleSuccess = (event: PrimerEvents['primer:payment-success']) => {
      const { payment } = event.detail;
      window.location.href = `/confirmation`;
    };

    const handleFailure = (event: PrimerEvents['primer:payment-failure']) => {
      const { error } = event.detail;
      console.error('Payment failed:', error.message);
    };

    checkout.addEventListener('primer:payment-success', handleSuccess);
    checkout.addEventListener('primer:payment-failure', handleFailure);

    return () => {
      checkout.removeEventListener('primer:payment-success', handleSuccess);
      checkout.removeEventListener('primer:payment-failure', handleFailure);
    };
  }, []);

  return (
    <primer-checkout
      ref={checkoutRef}
      client-token={clientToken}
    />
  );
}
```

### Custom styling in React

The `custom-styles` attribute accepts a JSON string. Pass it as a kebab-case attribute:

```tsx theme={"dark"}
const stylesJson = JSON.stringify({
  primerColorBrand: '#4a6cf7',
  primerRadiusMedium: '12px',
});

// Both React 18 and 19 — string attributes work the same
<primer-checkout
  client-token={clientToken}
  custom-styles={stylesJson}
/>
```

Alternatively, use CSS variables directly in your stylesheet (no `custom-styles` attribute needed):

```css theme={"dark"}
primer-checkout {
  --primer-color-brand: #4a6cf7;
  --primer-radius-medium: 12px;
}
```

### TypeScript setup

TypeScript doesn't recognize custom element tags. Add a declaration:

```typescript theme={"dark"}
// src/types/primer.d.ts
import type { CheckoutElement } from '@primer-io/primer-js';

declare global {
  namespace JSX {
    interface IntrinsicElements {
      'primer-checkout': CheckoutElement;
    }
  }
}
```

### React + SSR (Next.js)

Primer requires browser APIs. In Next.js, load it client-side only:

```tsx theme={"dark"}
'use client';

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

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

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

→ See the [SSR Guide](/checkout/primer-checkout/frameworks/ssr-guide) and [React Integration Guide](/checkout/primer-checkout/frameworks/react-integration-guide) for complete patterns.

***

## Vue 3

Vue has excellent Web Component support — it [scores 100%](https://custom-elements-everywhere.com/libraries/vue/results/results.html) on the Custom Elements Everywhere tests. Vue 3 automatically detects DOM properties using the `in` operator and sets them correctly, so passing objects works naturally.

### Compiler configuration

Vue attempts to resolve any tag with a dash as a Vue component first. Tell the compiler to treat `primer-*` tags as custom elements:

<Tabs>
  <Tab title="Vite (vite.config.js)">
    ```javascript theme={"dark"}
    import vue from '@vitejs/plugin-vue';

    export default {
      plugins: [
        vue({
          template: {
            compilerOptions: {
              isCustomElement: (tag) => tag.startsWith('primer-')
            }
          }
        })
      ]
    };
    ```
  </Tab>

  <Tab title="Vue CLI (vue.config.js)">
    ```javascript theme={"dark"}
    module.exports = {
      chainWebpack: (config) => {
        config.module
          .rule('vue')
          .use('vue-loader')
          .tap((options) => ({
            ...options,
            compilerOptions: {
              isCustomElement: (tag) => tag.startsWith('primer-')
            }
          }));
      }
    };
    ```
  </Tab>

  <Tab title="In-Browser">
    ```javascript theme={"dark"}
    // Only for in-browser compilation
    app.config.compilerOptions.isCustomElement = (tag) => tag.startsWith('primer-');
    ```
  </Tab>
</Tabs>

Without this configuration, Vue will emit "failed to resolve component" warnings for `<primer-checkout>` and other Primer elements.

### Basic integration

```vue theme={"dark"}
<template>
  <primer-checkout
    :client-token="clientToken"
    ref="checkoutRef"
  />
</template>

<script setup>
import { ref, onMounted } from 'vue';
import { loadPrimer } from '@primer-io/primer-js';

const props = defineProps({
  clientToken: { type: String, required: true },
});

const checkoutRef = ref(null);

// SDK options
const sdkOptions = {
  locale: 'en-GB',
  card: {
    cardholderName: { required: true, visible: true },
  },
};

onMounted(() => {
  loadPrimer();

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

  // Set options — Vue uses the `in` operator to detect this is a DOM property
  checkout.options = sdkOptions;

  // Payment events
  checkout.addEventListener('primer:payment-success', (event) => {
    const { payment } = event.detail;
    window.location.href = `/confirmation`;
  });

  checkout.addEventListener('primer:payment-failure', (event) => {
    const { error } = event.detail;
    console.error('Payment failed:', error.message);
  });
});
</script>
```

### Passing properties — Vue's `in` operator check

Vue 3 automatically checks whether a key exists as a DOM property using the `in` operator. If it does, Vue sets it as a property (not an attribute). This means objects like `options` work correctly in most cases.

However, if the `in` check fails (the property isn't defined when Vue first renders), you can force property binding with the `.prop` modifier:

```vue theme={"dark"}
<template>
  <!-- Standard binding — Vue auto-detects property vs attribute -->
  <primer-checkout
    :client-token="clientToken"
    :options="sdkOptions"
  />

  <!-- Force property binding if auto-detection fails -->
  <primer-checkout
    :client-token="clientToken"
    .options="sdkOptions"
  />
</template>
```

<Info>
  The `.prop` modifier (or its shorthand `.`) explicitly tells Vue to set a DOM property rather than an attribute. This is rarely needed for Primer components since Lit defines its properties upfront, but it's a useful escape hatch.
</Info>

### Custom styling in Vue

```vue theme={"dark"}
<template>
  <!-- Option 1: Attribute binding (must be a JSON string) -->
  <primer-checkout
    :client-token="clientToken"
    :custom-styles="JSON.stringify(customStyles)"
  />
</template>

<script setup>
const customStyles = {
  primerColorBrand: '#4a6cf7',
  primerRadiusMedium: '12px',
};
</script>
```

Or use CSS variables in a style block (preferred):

```vue theme={"dark"}
<style>
primer-checkout {
  --primer-color-brand: #4a6cf7;
  --primer-radius-medium: 12px;
}
</style>
```

### Vue 3 + SSR (Nuxt 3)

Primer requires browser APIs. In Nuxt 3, load it client-side only:

```vue theme={"dark"}
<template>
  <div>
    <primer-checkout
      v-if="isLoaded"
      :client-token="clientToken"
      ref="checkoutRef"
    />
  </div>
</template>

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

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

const props = defineProps({
  clientToken: { type: String, required: true },
});

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

You'll also need to register Primer elements as custom elements in `nuxt.config.ts`:

```typescript theme={"dark"}
// nuxt.config.ts
export default defineNuxtConfig({
  vue: {
    compilerOptions: {
      isCustomElement: (tag) => tag.startsWith('primer-')
    }
  }
});
```

→ See the [SSR Guide](/checkout/primer-checkout/frameworks/ssr-guide) for more Nuxt patterns.

***

## Other frameworks

### Svelte

Svelte treats custom elements natively — no special configuration is needed.

```svelte theme={"dark"}
<script>
  import { onMount } from 'svelte';

  export let clientToken;

  let checkoutEl;

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

    checkoutEl.options = {
      locale: 'en-GB',
    };

    checkoutEl.addEventListener('primer:payment-success', (event) => {
      const { payment } = event.detail;
      window.location.href = `/confirmation`;
    });

    checkoutEl.addEventListener('primer:payment-failure', (event) => {
      const { error } = event.detail;
      console.error('Payment failed:', error.message);
    });
  });
</script>

<primer-checkout
  bind:this={checkoutEl}
  client-token={clientToken}
/>
```

### Angular

Angular supports custom elements through `CUSTOM_ELEMENTS_SCHEMA`:

```typescript theme={"dark"}
// app.module.ts
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';

@NgModule({
  schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class AppModule {}
```

```typescript theme={"dark"}
// checkout.component.ts
import { Component, ElementRef, Input, OnInit, ViewChild, OnDestroy } from '@angular/core';
import { loadPrimer } from '@primer-io/primer-js';

@Component({
  selector: 'app-checkout',
  template: `
    <primer-checkout
      #checkoutRef
      [attr.client-token]="clientToken"
    ></primer-checkout>
  `
})
export class CheckoutComponent implements OnInit, OnDestroy {
  @Input() clientToken!: string;
  @ViewChild('checkoutRef') checkoutRef!: ElementRef;

  private successHandler = (event: PrimerEvents['primer:payment-success']) => {
    const { payment } = event.detail;
    // handle success
  };

  private failureHandler = (event: PrimerEvents['primer:payment-failure']) => {
    const { error } = event.detail;
    // handle failure
  };

  ngOnInit() {
    loadPrimer();

    const checkout = this.checkoutRef.nativeElement;
    checkout.options = { locale: 'en-GB' };

    checkout.addEventListener('primer:payment-success', this.successHandler);
    checkout.addEventListener('primer:payment-failure', this.failureHandler);
  }

  ngOnDestroy() {
    const checkout = this.checkoutRef?.nativeElement;
    if (checkout) {
      checkout.removeEventListener('primer:payment-success', this.successHandler);
      checkout.removeEventListener('primer:payment-failure', this.failureHandler);
    }
  }
}
```

***

## Migration checklist

Use this checklist to verify your migration is complete:

* **Updated `client-token` setting** — Using `setAttribute()` (vanilla JS), kebab-case attribute (React/Vue), or framework-appropriate binding
* **Updated `options` setting** — Using direct property assignment, not `setAttribute()`
* **Updated `custom-styles`** — Passing a JSON string via attribute, or using CSS variables instead
* **Updated event handlers** — Listening for `primer:payment-success` and `primer:payment-failure` events
* **Configured compiler** (Vue only) — Added `isCustomElement` for `primer-*` tags
* **SSR handled** (if applicable) — Loading Primer client-side only

***

## Next steps

<CardGroup cols={2}>
  <Card title="General Migration Guide" icon="arrows-rotate" href="/checkout/primer-checkout/migration/overview">
    API changes, payment method options, and event payloads
  </Card>

  <Card title="React Integration" icon="react" href="/checkout/primer-checkout/frameworks/react-integration-guide">
    React 18/19 patterns in depth
  </Card>

  <Card title="SSR Guide" icon="server" href="/checkout/primer-checkout/frameworks/ssr-guide">
    Next.js, Nuxt, and SvelteKit
  </Card>

  <Card title="SDK Options Reference" icon="gear" href="/sdk/primer-checkout-web/sdk-options-reference">
    Complete configuration reference
  </Card>
</CardGroup>
