npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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 modal
  • openModal() and initModal() 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-widget

The 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-widget and @infinitegiving/campaign-widget/vue anywhere 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.