stripe-pwa-elements
v4.0.3
Published
Framework-agnostic Stripe Payment Elements as Web Components
Maintainers
Readme
stripe-pwa-elements
Framework-agnostic Stripe payment components built with Web Components. Works with any web framework — React, Vue, Angular, Svelte, or plain HTML.
Note: The current npm package is
stripe-pwa-elements(this repo). The old package@stripe-elements/stripe-elements(v2.0.2) is deprecated and superseded — please usestripe-pwa-elementsinstead.
Try it first
Open the live demo in CodeSandbox (Stripe test mode, no setup or API keys needed) before installing anything. The demo covers:
<stripe-card-element>— card number / expiry / CVC with a PaymentIntent confirm flow<stripe-address-element>— billing address form<stripe-express-checkout-element>— Apple Pay / Google Pay / Link (device-dependent)
There is no <stripe-payment-element> demo yet. See examples/codesandbox for details.
Used in production
This library powers the Web implementation of @capacitor-community/stripe, which receives 4,000+ weekly downloads. If you use Capacitor for cross-platform apps, stripe-pwa-elements handles all Stripe UI on the web layer.
Manual follow-up (external repo): Add a reciprocal link in the capacitor-community/stripe README pointing back to this package.
Why stripe-pwa-elements?
Integrating Stripe from scratch requires wiring up several moving parts. This library collapses all of that into a single HTML element.
Before — raw @stripe/stripe-js (≈ 40 lines)
<div id="payment-element"></div>
<button id="pay" disabled>Pay</button>
<script type="module">
import { loadStripe } from 'https://esm.sh/@stripe/stripe-js';
// 1. Load Stripe.js
const stripe = await loadStripe('pk_test_xxxxx');
// 2. Create an Elements instance with your PaymentIntent client secret
const elements = stripe.elements({
clientSecret: 'pi_xxxxx_secret_xxxxx',
});
// 3. Create and mount the Payment Element
const paymentElement = elements.create('payment');
paymentElement.mount('#payment-element');
// 4. Enable the button once the element is ready
paymentElement.on('ready', () => {
document.getElementById('pay').disabled = false;
});
// 5. Handle form submission
document.getElementById('pay').addEventListener('click', async e => {
e.preventDefault();
// 6. Validate the payment details before confirming
const { error: submitError } = await elements.submit();
if (submitError) {
console.error(submitError.message);
return;
}
// 7. Confirm the payment (redirects on success)
const { error } = await stripe.confirmPayment({
elements,
confirmParams: {
return_url: window.location.href,
},
});
if (error) {
console.error(error.message);
}
});
</script>After — stripe-pwa-elements (≈ 5 lines)
<!-- One CDN line registers the custom element -->
<script type="module" src="https://unpkg.com/stripe-pwa-elements/dist/stripe-elements/stripe-elements.esm.js"></script>
<!-- Drop in the element — Stripe.js loading, Elements creation, mounting,
form submission, and confirmPayment are all handled for you -->
<stripe-payment-element
publishable-key="pk_test_xxxxx"
intent-client-secret="pi_xxxxx_secret_xxxxx"
></stripe-payment-element>The component emits a defaultFormSubmitResult event when the payment completes, so you can react to success or failure without writing any Stripe boilerplate.
Value props
- Framework-agnostic — Standard Web Components: drop into React, Vue, Angular, Svelte, or plain HTML with no adapter needed.
- No build step — one CDN line — Load via
<script type="module">from unpkg; no npm, no bundler, no configuration required. - PCI DSS SAQ-A eligible — All card data is entered inside Stripe-hosted iframes. Your page never touches raw card numbers, keeping you in the lightest PCI self-assessment tier.
Trust signal
Used in production as the web layer of @capacitor-community/stripe, which sees 4,000+ weekly npm downloads across Capacitor-powered iOS and Android apps.
Features
- Framework-agnostic — Standard Web Components that work everywhere
- Full Stripe Elements coverage — Card, Payment, Express Checkout, Address, and more
- Multiple payment flows — PaymentIntent, SetupIntent, and Checkout Session
- Apple Pay / Google Pay — Payment Request Button and Express Checkout support
- Internationalization — Built-in i18n support (English, Japanese, Portuguese)
- Lightweight — Minimal dependencies, lazy-loaded Stripe.js
Installation
npm install stripe-pwa-elementsUpgrading from Stripe.js 8
This package now uses Stripe.js 9 (Dahlia). Component methods and event names are unchanged, but Stripe objects exposed through events follow the Dahlia breaking changes:
- Replace direct
stripe.initCheckout()calls andStripeCheckoutimports withinitCheckoutElementsSdk()andStripeCheckoutElementsSdk. The component'sinitStripeWithCheckoutSession()handles this internally. - Handle the Promise returned by
elements.update()and replace booleanlayout.radiosoptions with supported string values. - Replace legacy Sources/Intent methods (including
createSourcein older examples) with current Payment Methods/Intent APIs. Direct Embedded Checkout integrations must usecreateEmbeddedCheckoutPage(). - The Address component explicitly requests localized values to preserve its existing
getValue()and submit results. Raw Stripe Address Element change events now return Latin-formattedaddress.statevalues. - If you load Stripe.js manually, use
https://js.stripe.com/dahlia/stripe.jsand align other Stripe.js dependencies to v9. Server API versions are managed separately; on Dahlia, this component's Checkout Sessions useui_mode: 'elements'.
Script tag
<script type="module" src="https://unpkg.com/stripe-pwa-elements/dist/stripe-elements/stripe-elements.esm.js"></script>ES Module
import { defineCustomElements } from 'stripe-pwa-elements/loader';
defineCustomElements();Bundlers (Vite, webpack, Capacitor)
When using a bundler — including Capacitor apps built with Vite — import the
components you need directly from the custom-elements build. Each module
auto-registers its element on import, so no defineCustomElements() call is needed:
import 'stripe-pwa-elements/dist/components/stripe-payment-element';This avoids the runtime dynamic import the lazy loader relies on, which can fail
with Failed to fetch dynamically imported module under Vite. See the
Bundler Integration guide
for Vite/webpack/Rollup and Capacitor setup examples.
Components
<stripe-card-element>
Stripe Card form with split fields (card number, expiry, CVC).
<stripe-payment-element>
Unified payment form supporting multiple payment methods.
<stripe-express-checkout-element>
Express Checkout with Apple Pay, Google Pay, Link, and more.
<stripe-payment-request-button>
(Beta) Payment Request API button (Apple Pay / Google Pay).
<stripe-address-element>
Address collection form with autocomplete.
<stripe-link-authentication-element>
Email collection with Stripe Link one-click checkout.
<stripe-currency-selector>
Currency selection component.
<stripe-modal>
Modal wrapper for payment components.
<stripe-card-element-modal>
Combined Card Element with Modal wrapper.
Quick Start
HTML
<stripe-card-element
publishable-key="pk_test_xxxxx"
intent-client-secret="pi_xxxxx_secret_xxxxx"
></stripe-card-element>
<script type="module" src="https://unpkg.com/stripe-pwa-elements/dist/stripe-elements/stripe-elements.esm.js"></script>
<script>
customElements.whenDefined('stripe-card-element').then(() => {
const element = document.querySelector('stripe-card-element');
element.addEventListener('formSubmit', async ({ detail }) => {
const { stripe, cardNumber } = detail;
const result = await stripe.createPaymentMethod({
type: 'card',
card: cardNumber,
});
console.log(result);
element.updateProgress('success');
});
});
</script>React
import { defineCustomElements } from 'stripe-pwa-elements/loader';
defineCustomElements();
function PaymentForm() {
const handleFormSubmit = (e) => {
const { stripe, cardNumber } = e.detail;
stripe.createPaymentMethod({ type: 'card', card: cardNumber });
};
return (
<stripe-card-element
publishable-key="pk_test_xxxxx"
intent-client-secret="pi_xxxxx_secret_xxxxx"
onFormSubmit={handleFormSubmit}
/>
);
}Vue
<template>
<stripe-card-element
publishable-key="pk_test_xxxxx"
intent-client-secret="pi_xxxxx_secret_xxxxx"
@formSubmit="handleFormSubmit"
/>
</template>
<script setup>
import { defineCustomElements } from 'stripe-pwa-elements/loader';
defineCustomElements();
const handleFormSubmit = (e) => {
const { stripe, cardNumber } = e.detail;
stripe.createPaymentMethod({ type: 'card', card: cardNumber });
};
</script>Use with AI assistants
The documentation site publishes machine-readable plain-text files that AI coding assistants (Claude Code, Cursor, GitHub Copilot, etc.) can use to answer questions about this library without hallucinating outdated API details.
| File | Purpose |
| --- | --- |
| /llms.txt | Index — links to the full and compact documentation sets |
| /llms-full.txt | Full API reference for all components |
| /llms-small.txt | Compact version with non-essential content removed |
These files are generated automatically from the documentation source at build time using the starlight-llms-txt plugin and always reflect the latest published docs.
Examples
See the example/ directory for complete working examples:
- Card Element (dynamic) — JavaScript-driven component creation
- Card Element (static) — Declarative HTML markup
- Payment Element — Unified payment form
- Link Authentication — Email + Link checkout
- Address Element — Address collection
- Express Checkout — Apple Pay, Google Pay, etc.
Use with coding agents (Agent Skill)
This repo ships an Agent Skill that teaches
coding agents how to integrate stripe-pwa-elements correctly — picking the right
component, wiring the publishable key + client secret, handling the
submit/confirm flow, and asking before anything that changes your Stripe account.
The skill lives in plugins/stripe-pwa-elements/skills/stripe-pwa-elements/.
Install it with the skills CLI:
npx skills add hideokamoto/stripe-pwa-elementsOr, in Claude Code, install it as a plugin:
/plugin marketplace add hideokamoto/stripe-pwa-elements
/plugin install stripe-pwa-elements@stripe-pwa-elementsOnce installed in Claude Code, the skill activates automatically when Claude detects you are
working with the library, or you can invoke it explicitly with
/stripe-pwa-elements:stripe-pwa-elements.
The documentation site also serves the skill over HTTP, built from the same file:
| URL | Purpose |
| --- | --- |
| /skill.md | The skill's SKILL.md |
| /.well-known/agent-skills/index.json | Discovery index (Agent Skills Discovery RFC, draft v0.2.0) with a SHA-256 digest per skill |
| /.well-known/agent-skills/stripe-pwa-elements/SKILL.md | The skill at the path the discovery index points to |
Contribution
Getting Started
npm install
npm startTo build the component for production, run:
npm run buildTo run the unit tests for the components, run:
npm testReal-browser checks against Stripe test mode
e2e/stripe/ holds Playwright checks that drive the built components in Chromium against the real Stripe.js, in Stripe test mode. They use the docs site's demo API (https://stripe-pwa-elements-docs.wp-kyoto.workers.dev/api) for the pk_test_ key and for new PaymentIntents, so no keys are needed. They need network access to js.stripe.com and to that API, so they are not part of CI.
pnpm run build # the harness serves www/build
pnpm exec playwright install chromium # once, if Playwright has no Chromium yet
pnpm run test:e2e:stripe- The harness is served on
http://localhost:4321, an origin the demo API allows. SetSTRIPE_E2E_PORTonly if the demo API allows that origin too. - To use an already installed Chromium, set
PLAYWRIGHT_BROWSERS_PATH, or pointSTRIPE_E2E_CHROMIUM_PATHat the executable. - Traces and screenshots of failed tests go to
test-reports/stripe-e2e/. - Type-check the suite with
pnpm exec tsc -p e2e/stripe/tsconfig.json.
To add a check: add a scenario to e2e/stripe/*.pw.ts using PaymentSheetHarness (e2e/stripe/support/payment-sheet.ts) and the demo API helpers (e2e/stripe/support/demo-api.ts). Element options and attributes go through harness.mount({ publishableKey, clientSecret, options, attributes }). For a different page, add an HTML file to e2e/stripe/pages/ that exposes the same window.harness contract and open it with harness.goto('/e2e/<file>.html').
Keep the Agent Skill up to date
When you add, rename or remove a component, or change its props, events, methods or how it is loaded,
update the Agent Skill in plugins/stripe-pwa-elements/skills/stripe-pwa-elements/
in the same pull request. src/agent-skill.unit.spec.ts fails when the skill's component tags,
CDN bundle path or i18n languages no longer match the code.
Security & PCI DSS SAQ-A
Card data never passes through this library's DOM, JavaScript state, or network requests. All sensitive input fields (card number, expiry, CVC) are rendered inside Stripe-hosted iframes, meaning your page never has access to raw card data. This architecture keeps integrations eligible for the lightest PCI DSS self-assessment questionnaire: SAQ-A.
How iframe isolation works
Your page (your domain)
┌─────────────────────────────────────────────────────────┐
│ <stripe-card-element> │
│ ┌───────────────────────────────────────────────────┐ │
│ │ <iframe src="https://js.stripe.com/..."> │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ Card number [____ ____ ____ ____] │ │ │
│ │ │ Expiry [MM/YY] CVC [___] │ │ │
│ │ │ │ │ │
│ │ │ (Stripe's origin — cross-origin boundary) │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────┘ │
│ │
│ Your JS can call stripe.confirmPayment() but │
│ CANNOT read card values from the iframe. │
└─────────────────────────────────────────────────────────┘- The browser's same-origin policy prevents any JavaScript on your page from reading input values inside Stripe's iframe.
- Card data travels directly from the iframe to Stripe's servers over TLS — it never touches your application server.
- This library only manages the mounting, styling, and event wiring of those iframes; it does not alter Stripe's security model.
For a complete explanation see Stripe's SAQ-A documentation.
For the vulnerability reporting channel and supply-chain notes (npm provenance), see SECURITY.md.
Sponsors
If stripe-pwa-elements saves you time, please consider sponsoring the lead maintainer:
Public API & TypeScript types
All user-facing TypeScript types are exported directly from the package root. Import them using the package name:
import type {
FormSubmitEvent,
FormSubmitHandler,
StripeLoadedEvent,
StripeDidLoadedHandler,
ProgressStatus,
IntentType,
InitStripeOptions,
LinkAuthenticationElementChangeEvent,
LinkAuthenticationElementChangeHandler,
CurrencySelectorChangeEvent,
CurrencySelectorChangeHandler,
DefaultFormSubmitResult,
} from 'stripe-pwa-elements';The custom-elements entry stripe-pwa-elements/dist/components/<tag> is a supported import path for bundler-based setups (see Bundlers above). However, do not import from internal paths such as dist/types/interfaces. Those paths are implementation details and are not part of the public API — they may change without notice across any release.
Semver policy
All types exported from the package root (stripe-pwa-elements) are considered public API and follow Semantic Versioning. Within the ^3.0.0 range:
- No breaking changes will be made to the shape of any publicly exported type.
- New optional fields may be added to event objects without a major bump.
- Removals or renames of exported types always require a new major version.
Maintainers
| Maintainer | GitHub | Social | | --- | --- | --- | | Hidetaka Okamoto | hideokamoto | @hide__dev | | Masaki Hirano | contiki9 | @maki_saki | | Masahiko Sakakibara | rdlabo | @rdlabo |
License
MIT
