Skip to main content
Primer Checkout is built with Lit 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.
This guide covers framework-specific patterns. For the complete list of API and configuration changes (callbacks, payment method options, event payloads, vault methods), see the General Migration Guide.

Quick comparison

TaskVanilla JSReact 18React 19Vue 3
Set client-tokensetAttribute()client-token={val}client-token={val}:client-token="val"
Set options objectel.options = objref + useEffectJSX prop directly.options="obj" or ref
Set custom-stylessetAttribute() + JSONcustom-styles={json}custom-styles={json}:custom-styles="json"
Listen to eventsaddEventListener()ref + useEffectref + useEffect@primer:payment-success or ref
Handle payment resultprimer:payment-success eventEvent in useEffectEvent in useEffectEvent 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:
TypeHow to setExamples
Component properties (strings)setAttribute() or HTML attributeclient-token, custom-styles, loader-disabled
SDK options (objects)Direct property assignmentoptions
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

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);
});
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():
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 JSXWhat React doesResult
clientToken="..."Sets attribute clienttoken (lowercased)❌ Wrong attribute name
client-token="..."Sets attribute client-token✅ Works
ref.current.clientToken = valueSets 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.
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<HTMLElement>(null);

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

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

    const handleSuccess = (event: CustomEvent) => {
      const { payment } = event.detail;
      window.location.href = `/confirmation`;
    };

    const handleFailure = (event: CustomEvent) => {
      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.
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<HTMLElement>(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: CustomEvent) => {
      const { payment } = event.detail;
      window.location.href = `/confirmation`;
    };

    const handleFailure = (event: CustomEvent) => {
      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:
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):
primer-checkout {
  --primer-color-brand: #4a6cf7;
  --primer-radius-medium: 12px;
}

TypeScript setup

TypeScript doesn’t recognize custom element tags. Add a declaration:
// 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:
'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 and React Integration Guide for complete patterns.

Vue 3

Vue has excellent Web Component support — it scores 100% 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:
import vue from '@vitejs/plugin-vue';

export default {
  plugins: [
    vue({
      template: {
        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

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

<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):
<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:
<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:
// nuxt.config.ts
export default defineNuxtConfig({
  vue: {
    compilerOptions: {
      isCustomElement: (tag) => tag.startsWith('primer-')
    }
  }
});
→ See the SSR Guide for more Nuxt patterns.

Other frameworks

Svelte

Svelte treats custom elements natively — no special configuration is needed.
<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:
// app.module.ts
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';

@NgModule({
  schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class AppModule {}
// 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: CustomEvent) => {
    const { payment } = event.detail;
    // handle success
  };
  
  private failureHandler = (event: CustomEvent) => {
    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