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

> [!NOTE]
>
> 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](https://primer.io/docs/checkout/primer-checkout/migration/overview.md) for platform-specific migration instructions.

> [!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](https://primer.io/docs/checkout/primer-checkout/migration/overview.md).

***

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

### 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);
});
```

> [!NOTE]
>
> **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.

### 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](https://primer.io/docs/checkout/primer-checkout/frameworks/ssr-guide.md) and [React Integration Guide](https://primer.io/docs/checkout/primer-checkout/frameworks/react-integration-guide.md) 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:

> [tab] 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] 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] In-Browser
>
> ```javascript theme={"dark"}
> // Only for in-browser compilation
> app.config.compilerOptions.isCustomElement = (tag) => tag.startsWith('primer-');
> ```

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>
```

> [!NOTE]
>
> 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.

### 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](https://primer.io/docs/checkout/primer-checkout/frameworks/ssr-guide.md) 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

> [card] [General Migration Guide](https://primer.io/docs/checkout/primer-checkout/migration/overview.md)
>
> API changes, payment method options, and event payloads

> [card] [React Integration](https://primer.io/docs/checkout/primer-checkout/frameworks/react-integration-guide.md)
>
> React 18/19 patterns in depth

> [card] [SSR Guide](https://primer.io/docs/checkout/primer-checkout/frameworks/ssr-guide.md)
>
> Next.js, Nuxt, and SvelteKit

> [card] [SDK Options Reference](https://primer.io/docs/sdk/primer-checkout-web/sdk-options-reference.md)
>
> Complete configuration reference


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.