Skip to content

The button

Two zero-JavaScript entry points, both wired up by core/klappay-one.ts under the hood — neither is a second implementation, both just build a KlappayOneConfig from attributes and call createKlappayOne(config).open().

<klappay-button>

A real Custom Element (customElements.define('klappay-button', ...)), rendered inside a Shadow DOM so neither the host page's CSS nor Klappay's own leaks across the boundary:

html
<klappay-button
  charge-id="ch_123"
  origin="https://klap.one"
  variant="black"
  size="md"
  locale="en"
  mode="iframe"
></klappay-button>
AttributeRequiredDescription
charge-idYesThe Charge this checkout is for.
originOnly if not configure()'dWhich Klappay origin to open.
variantNowhite | yellow | black — defaults to black. See Styling.
sizeNosm | md | lg — defaults to md. See Styling.
localeNoForwarded to the checkout — falls back to configure()'s locale.
modeNoiframe | popup — forces a mode instead of the device default.

variant/size are reactive — changing either attribute after the element is already on the page (el.setAttribute('variant', 'white')) updates the rendered button immediately, via attributeChangedCallback.

Events

ts
const button = document.querySelector('klappay-button')
button.addEventListener('success', (event) => console.log(event.detail)) // PaymentResult
button.addEventListener('error', (event) => console.log(event.detail)) // KlappayOneError
button.addEventListener('cancel', () => console.log('payer closed the checkout'))

A second click before the first checkout settles is ignored — the button disables itself (this.#button.disabled = true) the moment it opens the popup/iframe, and re-enables on whichever of success/error/cancel fires first. That's what stops a fast double-click from opening two popups/iframes stacked on top of each other.

Missing charge-id or origin (and no configure() default) logs a console.error and does nothing on click — it never throws, so one misconfigured button on a page doesn't take the rest of the page down with it.

Your own button: data-klappay-one

For when you already have a button and don't want a second custom element in your markup — any clickable element works, not just <button>:

html
<button data-klappay-one="ch_123" data-klappay-one-origin="https://klap.one">
  Pay with Klappay
</button>
AttributeRequiredDescription
data-klappay-oneYesThe chargeId — also what marks the element for auto-wiring.
data-klappay-one-originOnly if not configure()'dWhich Klappay origin to open.
data-klappay-one-localeNoFalls back to configure()'s locale.
data-klappay-one-modeNoiframe | popup — forces a mode instead of the device default.

Same success/error/cancel CustomEvents as <klappay-button> above, dispatched on the element itself. There's no variant/size here — this path renders nothing, it only adds a click handler to markup you already control, so styling is entirely up to your own CSS.

While a checkout is in flight the element carries a data-klappay-one-busy attribute (added on click, removed on success/error/cancel) — the same double-click guard as <klappay-button>, just expressed as an attribute instead of the native disabled property, since the wired element isn't necessarily a form control.

css
[data-klappay-one][data-klappay-one-busy] {
  opacity: 0.6;
  pointer-events: none;
}

Elements added after the script loads

Both wireExisting() (run once on load) and a MutationObserver (observeNewElements(), watching document.body for the lifetime of the page) wire up [data-klappay-one] elements — so a button rendered by a client-side router, injected by a third-party script, or added inside a modal opened later all get wired automatically, with no manual re-wire() call needed anywhere in your code.

Choosing between the two

Reach for <klappay-button> when you want Klappay's own button styling (pick a variant/size and move on) — it's the fastest path and the one the button preview in klap-app matches exactly. Reach for data-klappay-one when the button already needs to match a design system you don't control from here — your own markup, your own CSS, this package only adds the click handler.

Docs live in ./docs — the source of truth for both the package and this site.