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
| 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 |
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:
- CamelCase props are lowercased. Writing
<primer-checkout clientToken="..." /> sets the attribute clienttoken (all lowercase), which the component doesn’t recognize.
- 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.
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:
Vite (vite.config.js)
Vue CLI (vue.config.js)
In-Browser
import vue from '@vitejs/plugin-vue';
export default {
plugins: [
vue({
template: {
compilerOptions: {
isCustomElement: (tag) => tag.startsWith('primer-')
}
}
})
]
};
module.exports = {
chainWebpack: (config) => {
config.module
.rule('vue')
.use('vue-loader')
.tap((options) => ({
...options,
compilerOptions: {
isCustomElement: (tag) => tag.startsWith('primer-')
}
}));
}
};
// 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
<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