@infinitegiving/campaign-widget
v0.0.17
Published
Embed the Infinite Giving donation experience on your site with web components.
Readme
@infinitegiving/campaign-widget
Embed the Infinite Giving donation experience on your site with web components.
This package provides:
<ig-widget-button>for a ready-made button that opens the donation flow in a modalopenModal()andinitModal()for programmatic modal control<ig-widget>for an inline embedded donation flow
You will need a valid campaign ID from Infinite Giving. Each campaign you run has its own ID — a single nonprofit may have several.
Installation
npm install @infinitegiving/campaign-widgetThe package is self-contained — everything it needs is bundled, so there is nothing else to install. If your app already runs Vue 3.5+, see Vue 3 for an entry point that reuses your copy of Vue instead of bundling its own.
TypeScript declarations are included; no @types/… package is needed.
Quick start
Import the package once so the custom elements are registered:
import '@infinitegiving/campaign-widget'Modal button
<ig-widget-button campaign-id="YOUR_CAMPAIGN_ID" label="Donate Now"></ig-widget-button>Programmatic modal
import { openModal } from '@infinitegiving/campaign-widget'
openModal({ campaignId: 'YOUR_CAMPAIGN_ID', env: 'production' })Inline embed
<ig-widget campaign-id="YOUR_CAMPAIGN_ID" env="production"></ig-widget>Choose an integration
Use this package in the way that fits your site:
- Use
<ig-widget-button>for a built-in donate button that opens a modal - Use
openModal()to open the modal from your own button or code - Use
initModal()to attach modal behavior to existing DOM elements by selector - Use
<ig-widget>to render the donation flow directly in the page
Reference
<ig-widget-button>
Renders a default button that opens the donation flow in a modal.
Attributes
| Attribute | Required | Description |
| ------------- | -------- | --------------------------------------------- |
| campaign-id | Yes | Your Infinite Giving campaign ID |
| label | No | Button text. Defaults to Donate Now |
| env | No | Environment to use: production or sandbox |
Example
<ig-widget-button
campaign-id="YOUR_CAMPAIGN_ID"
label="Support our mission"
env="production"
></ig-widget-button><ig-widget>
Renders the donation flow inline in the page. Give the widget at least 320px of width for the best layout.
Attributes
| Attribute | Required | Description |
| ------------- | -------- | --------------------------------------------- |
| campaign-id | Yes | Your Infinite Giving campaign ID |
| mode | No | embed (default) or modal |
| env | No | Environment to use: production or sandbox |
Use kebab-case attribute names in HTML: campaign-id, not campaignId.
Example
<ig-widget campaign-id="YOUR_CAMPAIGN_ID" env="production"></ig-widget>To open as a modal immediately when rendered:
<ig-widget campaign-id="YOUR_CAMPAIGN_ID" mode="modal" env="production"></ig-widget>JavaScript helpers
The package also exports modal helpers. Modal instances close on Escape or when the close button is clicked.
openModal(options)
Opens the donation flow as a modal.
import { openModal } from '@infinitegiving/campaign-widget'
openModal({ campaignId: 'YOUR_CAMPAIGN_ID', env: 'production' })initModal(options)
Attaches click handlers to existing DOM elements that will open the modal. Call this once, after your trigger elements are in the DOM.
import { initModal } from '@infinitegiving/campaign-widget'
initModal({
campaignId: 'YOUR_CAMPAIGN_ID',
trigger: '.js-open-donate-modal',
env: 'production',
})initModal binds handlers to elements matching trigger at the time it's called. Elements added to the DOM later will not be bound — for dynamic content, call openModal from your own click handler instead.
Options
| Option | Required | Default | Description |
| ------------ | --------------------- | -------------------------- | ---------------------------------------------------- |
| campaignId | Yes | — | Your Infinite Giving campaign ID |
| trigger | No (initModal only) | [data-ig-widget-trigger] | CSS selector for elements that should open the modal |
| env | No | — | Environment to use: production or sandbox |
Events
The widget dispatches native DOM CustomEvents as the donor moves through a flow, so you can
react on your own page.
Because these are standard DOM events, they work with any framework — Vue, React, Angular, or
plain JavaScript.
Every event name is prefixed with ig:, and the payload is available directly on event.detail.
Events bubble and cross the shadow DOM boundary, so listen for them on document. Delegating on
document means you don't need a reference to the widget element.
document.addEventListener('ig:donation-created', (e) => {
console.log('Donation created', e.detail)
})Available events
| Event | Fires when | event.detail |
| --------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| ig:widget-loaded | The campaign has loaded and the widget is ready | { campaignId, organizationName? } |
| ig:donation-type-selected | The donor picks a giving method on the "more ways to give" screen | { type: 'stock' \| 'crypto' \| 'daf' \| 'endowment' } |
| ig:donation-created | A donation record is created on the server | { type: 'cash' \| 'crypto', donation } — or { type: 'stock', donations: [...] } for a batch |
| ig:donation-error | A (non-validation) request fails | { message, code?, flow?, action? } |
Fields marked ? are only present when available for that event. Inline field-validation errors
are handled inside the widget and do not emit ig:donation-error.
For ig:donation-created, switch on event.detail.type rather than inspecting the shape: cash and
crypto carry a single donation record, while stock carries a donations array (one entry per
asset, since a stock gift can span multiple tickers).
Examples
HTML
<script>
document.addEventListener('ig:donation-created', (e) => {
console.log('Donation created', e.detail)
})
</script>React
import { useEffect } from 'react'
export function DonationEvents() {
useEffect(() => {
const onCreated = (e: Event) => console.log('created', (e as CustomEvent).detail)
document.addEventListener('ig:donation-created', onCreated)
return () => document.removeEventListener('ig:donation-created', onCreated)
}, [])
return null
}Vue
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue'
const onCreated = (e: Event) => console.log('created', (e as CustomEvent).detail)
onMounted(() => document.addEventListener('ig:donation-created', onCreated))
onUnmounted(() => document.removeEventListener('ig:donation-created', onCreated))
</script>HTML / static sites
Import the package once with a module script, then use the custom elements in your markup.
Modal button
<script type="module">
import '@infinitegiving/campaign-widget'
</script>
<ig-widget-button
campaign-id="YOUR_CAMPAIGN_ID"
label="Donate now"
env="production"
></ig-widget-button>Programmatic modal
<button id="donate-button" type="button">Donate</button>
<script type="module">
import { openModal } from '@infinitegiving/campaign-widget'
document.getElementById('donate-button')?.addEventListener('click', () => {
openModal({ campaignId: 'YOUR_CAMPAIGN_ID', env: 'production' })
})
</script>Selector-based modal binding
The default [data-ig-widget-trigger] attribute is the lowest-friction option for static sites — add it to any element and call initModal once.
<button data-ig-widget-trigger type="button">Donate</button>
<script type="module">
import { initModal } from '@infinitegiving/campaign-widget'
initModal({ campaignId: 'YOUR_CAMPAIGN_ID' })
</script>Or use a custom selector:
<button class="js-open-donate-modal" type="button">Donate</button>
<script type="module">
import { initModal } from '@infinitegiving/campaign-widget'
initModal({
campaignId: 'YOUR_CAMPAIGN_ID',
trigger: '.js-open-donate-modal',
})
</script>Inline embed
<script type="module">
import '@infinitegiving/campaign-widget'
</script>
<ig-widget campaign-id="YOUR_CAMPAIGN_ID" env="production"></ig-widget>React
Import the package once near your app entry so the custom elements are defined before render.
import '@infinitegiving/campaign-widget'Built-in modal button
export function DonateButton() {
return <ig-widget-button campaign-id="YOUR_CAMPAIGN_ID" label="Donate now" env="production" />
}Programmatic modal
import { openModal } from '@infinitegiving/campaign-widget'
export function DonateButton() {
return (
<button
type="button"
onClick={() => openModal({ campaignId: 'YOUR_CAMPAIGN_ID', env: 'production' })}
>
Donate
</button>
)
}Selector-based modal binding
import { useEffect } from 'react'
import { initModal } from '@infinitegiving/campaign-widget'
export function DonateModalBindings() {
useEffect(() => {
initModal({ campaignId: 'YOUR_CAMPAIGN_ID', trigger: '[data-open-donate]' })
}, [])
return (
<button type="button" data-open-donate>
Donate
</button>
)
}Inline embed
export function DonationPanel() {
return <ig-widget campaign-id="YOUR_CAMPAIGN_ID" env="production" />
}SSR note
For SSR frameworks such as Next.js or Remix, load the package on the client only — the widget expects window and the DOM. In Next.js App Router, put 'use client' at the top of the file that imports the package. In Remix, import from a useEffect or a client-only component.
Vue 3
Import the package once before mounting your app. Vue apps should use the /vue entry point, which treats Vue as an external dependency and reuses the copy already in your app instead of bundling a second one:
import '@infinitegiving/campaign-widget/vue'Requires Vue 3.5 or later, declared as an optional peer dependency. Everything else — Pinia, Vue Router, Vue I18n — stays bundled inside the widget, which runs its own isolated instance of each.
Use one entry point consistently. Importing both
@infinitegiving/campaign-widgetand@infinitegiving/campaign-widget/vueanywhere in the same app loads the widget twice. The first one to load wins the custom-element registration, and functions imported from the other would act on a different instance.
The plain @infinitegiving/campaign-widget entry still works in Vue and needs no peer dependency.
Tell Vue to treat ig-* tags as custom elements, otherwise Vue will log console warnings and try to resolve them as Vue components. This applies to both entry points.
Vite example
import vue from '@vitejs/plugin-vue'
export default {
plugins: [
vue({
template: {
compilerOptions: {
isCustomElement: (tag) => tag.startsWith('ig-'),
},
},
}),
],
}Built-in modal button
<template>
<ig-widget-button campaign-id="YOUR_CAMPAIGN_ID" label="Donate now" env="production" />
</template>Programmatic modal
<script setup lang="ts">
import { openModal } from '@infinitegiving/campaign-widget/vue'
function donate() {
openModal({ campaignId: 'YOUR_CAMPAIGN_ID', env: 'production' })
}
</script>
<template>
<button type="button" @click="donate">Donate</button>
</template>Selector-based modal binding
<script setup lang="ts">
import { onMounted } from 'vue'
import { initModal } from '@infinitegiving/campaign-widget/vue'
onMounted(() => {
initModal({ campaignId: 'YOUR_CAMPAIGN_ID', trigger: '.js-open-donate-modal' })
})
</script>
<template>
<button type="button" class="js-open-donate-modal">Donate</button>
</template>Inline embed
<template>
<ig-widget campaign-id="YOUR_CAMPAIGN_ID" env="production" />
</template>Nuxt / SSR note
For Nuxt or other SSR setups, load the package on the client only — either inside onMounted or behind a <ClientOnly> wrapper.
TypeScript
Declarations ship with the package and are picked up automatically from whichever entry point you import.
Importing the package also augments the DOM lib, so these work without any extra setup:
// document.querySelector knows the tag
const widget = document.querySelector('ig-widget')
if (widget) widget.initialState = 'stock'
// events are typed, on the element or delegated from document
document.addEventListener('ig:donation-created', (event) => {
if (event.detail.type === 'stock') {
console.log(event.detail.donations.length) // stock carries a batch
} else {
console.log(event.detail.donation) // cash and crypto carry one record
}
})Vue templates
The /vue entry additionally registers <ig-widget> and <ig-widget-button> with Vue, so their props are checked in templates. This is separate from the isCustomElement compiler option, which is still required — see Vue 3.
React JSX
React does not know these tags by default. Add a declaration once, anywhere in your project:
declare module 'react' {
namespace JSX {
interface IntrinsicElements {
'ig-widget': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & {
'campaign-id': string
theme?: 'light' | 'dark'
mode?: 'embed' | 'modal'
env?: 'production' | 'sandbox'
initialState?: 'cash' | 'stock' | 'crypto' | 'daf' | 'endowment' | 'more-ways-to-give'
}
'ig-widget-button': React.DetailedHTMLProps<
React.HTMLAttributes<HTMLElement>,
HTMLElement
> & {
'campaign-id': string
theme?: 'light' | 'dark'
label?: string
env?: 'production' | 'sandbox'
}
}
}
}We do not ship this, because the correct form of the augmentation differs between React 17, 18 and 19.
Support
For campaign IDs, integration support, or provisioning help, contact Infinite Giving.
