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

@bro-code/checkout

v2026.0.0

Published

A themed Stripe checkout dialog for any web app: Stripe's Checkout Elements in a framework-free dialog, with a theme for each app.

Readme

@bro-code/checkout

npm version license

A themed Stripe checkout dialog for any web app: Stripe's Checkout Elements in a framework-free dialog, with a theme for each app.

One call opens a dialog over the app with:

  • the order summary: each item, any discount and tax added on top, the total (with the tax a tax-included price holds), and what a subscription costs after today (after a free trial, when it has one);
  • an email field, when the Checkout Session has no email yet;
  • Stripe's Billing Address Element, on every checkout: the session doesn't say whether it requires the billing address, and one that does can't be paid without it;
  • Stripe's Payment Element, themed to match the app;
  • a Pay button that says what's paid (Pay $4.99, Start free trial, or Confirm when nothing is due today), and any decline or error under it.

It resolves when the payment is confirmed, so the player never leaves the app for Stripe's hosted page. It works with any library or none, and does nothing when imported: Stripe.js loads from js.stripe.com on the first checkout. Without a window (server rendering), createCheckout does nothing and returns a controller whose open resolves closed.

Installation

npm install @bro-code/checkout

@stripe/stripe-js, Stripe's own loader, comes with it.

Usage

Keep the settings in the app's bro.config.json:

{
  "checkout": {
    "theme": "rombie"
  }
}

A browser can't read that file at runtime, so import the section and pass it in, with the parts only code can supply (TypeScript needs resolveJsonModule, which moduleResolution: "bundler" turns on; Vite keeps only the imported section in the bundle):

import { createCheckout } from '@bro-code/checkout';
import { checkout as checkoutConfig } from '../bro.config.json';

const checkout = createCheckout({
  ...checkoutConfig,
  // Test and live keys differ, so the key comes from the app's env.
  publishableKey: import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY,
});

A CommonJS build is included too: const { createCheckout } = require('@bro-code/checkout');.

Call it once, at startup. A second call (a dev server's hot reload, or the app switching between dark and light) replaces the options, restyles a checkout that's open, and returns the same controller.

Then open a checkout for a session the app's server created:

const result = await checkout.open({
  // Called when the dialog opens, so it shows while the server works.
  clientSecret: () => api.startCheckout(slug).then((response) => response.clientSecret),
});

if (result.status === 'complete') {
  // Tell the server, as a return from Stripe's hosted page would.
  await api.confirmPurchase(slug, result.sessionId);
}

open resolves { status: 'complete', sessionId, session } when the payment is confirmed and { status: 'closed' } when the player closes the dialog. It rejects, after closing the dialog, when the checkout can't start: the clientSecret function rejects (its own error comes through, so the app can show it as it does now), Stripe.js can't load, or the session can't be loaded or has expired. A declined card isn't a rejection: the dialog shows Stripe's message and the player can try again.

checkout.close(); // resolves the open checkout's promise with { status: 'closed' }

While a payment is being confirmed, close(), Escape and the close button do nothing, and a second open rejects.

To show the form in the app's own page instead of a dialog, give it a container. It has no close button there; close() ends it and empties the container.

await checkout.open({ clientSecret, container: document.querySelector('#pay')! });

Options

In bro.config.json (CheckoutConfig)

| Field | Default | | |---|---|---| | theme | default | default, brocode, rombie, taut-wad or custom. See Themes. | | mode | dark | dark or light: which of the theme's two versions to use. | | colors | | With theme: 'custom', any of the ten theme values over default's. | | fonts | | [{ "cssSrc": "<stylesheet URL>" }]: stylesheets with @font-face rules for Stripe's fields. See Fonts. |

Code only (CheckoutOptions)

| Field | | |---|---| | publishableKey | Required. The Stripe account's publishable key, pk_test_... or pk_live_.... Anything else throws a TypeError. | | appearance | Stripe Appearance API settings over the theme's, for Stripe's fields only: variables and rules are merged, anything else replaced. | | onOpenChange | (open: boolean) => void, called when a checkout opens and closes. |

Anything else invalid falls back with a [checkout] warning in the console.

Each checkout (CheckoutRequest)

| Field | | |---|---| | clientSecret | Required. The client secret of a Checkout Session created with ui_mode: 'elements', or a function that returns a promise of one. | | title | The heading. Default: the first line item's name (Checkout while it loads). | | returnUrl | Where the browser comes back to after a payment method that has to leave the page (a bank redirect). Default: the session's return_url. Cards and wallets never leave. | | container | An element to show the form in, instead of the dialog. |

Themes

Each theme has a dark and a light version. rombie has no light theme, so it uses its dark one in both modes; taut-wad uses brocodetech's palette, as the app does.

| Value | default dark / light | brocode and taut-wad dark / light | rombie | |---|---|---|---| | background | #121417 / #ffffff | #181818 / #dddddd | #0a0a12 | | surface | #1c1f24 / #f4f6f8 | #222222 / #eeeeee | #15141f | | text | #e8eaed / #1f2328 | #ffffff / #000000 | #f1eef9 | | muted | #9aa0a6 / #59636e | #b0b0b0 / #5f5f5f | #9a90bd | | border | #33373d / #d0d7de | #3c3c3c / #bdbdbd | #302c47 | | accent | #5b9cff / #1a66d9 | #7b9981 / #7b9981 | #ff2d78 | | accentText | #0b1220 / #ffffff | #ffffff / #ffffff | #12060d | | error | #ff6b6b / #c62828 | #ff5252 / #b95252 | #ff5c5c | | success | #6fcf97 / #1e7e34 | #96be4d / #7a9a3f | #00e5ff | | font | system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif | Roboto, sans-serif | system-ui, 'Segoe UI', Roboto, sans-serif |

custom starts from default in the mode given:

{ "checkout": { "theme": "custom", "mode": "light", "colors": { "accent": "#e91e63", "font": "Georgia, serif" } } }

Each color is checked with CSS.supports('color', ...) and the font with CSS.supports('font-family', ...); a missing or invalid one keeps default's, with a warning for the invalid ones. An unknown theme is default, and an unknown mode dark, with a warning.

To follow an app that switches between dark and light, call createCheckout again with the new mode:

watch(isDark, (dark) => createCheckout({ ...options, mode: dark ? 'dark' : 'light' }));

How a theme reaches Stripe's fields

Stripe's fields are iframes from js.stripe.com, so the page's CSS can't reach them. The theme is passed to Stripe as Appearance API settings (toAppearance gives them, for an app that builds its own Stripe form):

  • night is the base in dark mode and stripe in light, for anything the variables don't cover;
  • accent is colorPrimary (and accentText is colorPrimaryText), surface is colorBackground (the fields), text is colorText, muted is colorTextSecondary and the placeholder, error is colorDanger, success is colorSuccess, and font is fontFamily;
  • fields, tabs and blocks get a 1px border and an accent ring when focused, and every radius is 8px, the same as the dialog's.

Fonts

Stripe's fields can't use the page's web fonts. Without fonts, they use the first font in the theme's font that the device has (Roboto where it's installed, else the system's sans-serif). To load one, give Stripe a stylesheet with its @font-face rules:

{ "checkout": { "theme": "brocode", "fonts": [{ "cssSrc": "https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;600&display=swap" }] } }

Restyling

The dialog is in the page's own DOM, not a shadow root, because Stripe doesn't support its fields in one. Its rules are all under .bc-checkout, so a page's element rules (a global button { }) don't reach it, and its theme is set as custom properties on the .bc-checkout root, which a page rule with !important overrides:

.bc-checkout {
  --bcc-accent: #e91e63 !important;
}

The properties are --bcc-background, --bcc-surface, --bcc-text, --bcc-muted, --bcc-border, --bcc-accent, --bcc-accent-text, --bcc-error, --bcc-success and --bcc-font. They style the dialog around Stripe's fields; restyle the fields themselves with appearance. The root also carries data-mode="dark" or "light".

The server's part

The app's server creates the Checkout Session with Stripe's API and returns only its client secret. With stripe-node:

const session = await stripe.checkout.sessions.create({
  ui_mode: 'elements',
  mode: 'payment',
  customer_email: user.email, // or customer: ...; without either, the dialog asks for an email
  line_items: [{ price_data: { currency: 'usd', unit_amount: 499, product_data: { name: 'StarShip RPG' } }, quantity: 1 }],
  return_url: 'https://app.example.com/store?session_id={CHECKOUT_SESSION_ID}',
  metadata: { userId: String(user.id) },
});
res.json({ clientSecret: session.client_secret });
  • ui_mode: 'elements' needs Stripe API version 2026-03-25.dahlia or later. Dahlia renamed it from custom, and custom fails from then on. An older version pinned for the rest of the server can be overridden on this one call: stripe.checkout.sessions.create(params, { apiVersion: '2026-03-25.dahlia' }).
  • A session for a subscription (mode: 'subscription', with a trial or without) works the same way.
  • For sales tax through Stripe Tax, add automatic_tax: { enabled: true } and billing_address_collection: 'required'. Stripe then wants the buyer's full billing address, which the dialog's address fields above the payment fields collect, and the tax line appears once Stripe has the address. A session for an existing customer also needs customer_update: { address: 'auto' }, so Stripe can save the address onto the customer.
  • The session's return_url replaces success_url and cancel_url: there's nothing to cancel back to, since the player never leaves, and return_url is only used by a payment method that has to.
  • Keep fulfilling from Stripe's checkout.session.completed webhook. open resolving complete is the moment to tell the server and update the page, but the server should check the session with Stripe before granting anything.

Content Security Policy

A site with a CSP must allow Stripe.js, as Stripe's CSP list says: https://js.stripe.com in script-src and frame-src, and https://api.stripe.com in connect-src, plus the hosts for 3-D Secure and any wallets the account uses. The dialog's own styles are a constructable stylesheet where the browser has them, which style-src allows without 'unsafe-inline'.

API

| Export | | |---|---| | createCheckout(options) | Sets everything up and returns { open(request), close() }. | | toAppearance(colors, mode, override?) | A theme's colors as Stripe Appearance API settings, as the dialog gives them to Stripe. | | THEMES | Each preset's dark and light colors. | | resolveColors(config) | The colors a config's theme, mode and colors give, as the dialog uses them. | | Types | CheckoutConfig, CheckoutOptions, CheckoutRequest, CheckoutResult, CheckoutController, CheckoutTheme, CheckoutMode, CheckoutColors. |

The dialog is role="dialog" with aria-modal="true", labelled by its heading. While it's open, the app's own elements in document.body are made inert, so focus stays in the dialog, and focus goes back to what opened it when it closes; Escape closes it. It isn't a native modal <dialog>, because showModal() makes the rest of the page inert, and Stripe adds 3-D Secure's challenge to the page's body. Stripe's own frames in the body are never made inert.

License

MIT - see LICENSE.

Changelog

[2026.0.0] - 2026-11-09

Added

  • Initial release.
  • createCheckout(options), with open(request) and close(): Stripe's Checkout Elements in a themed dialog, or in a container.
  • The order summary (line items, discount, tax, total, a subscription's price after today and its trial), an email field when the session has none, the Payment Element and the Pay button.
  • Sales tax: the Billing Address Element on every checkout, since a session that requires the billing address (automatic_tax with billing_address_collection: 'required') can't be confirmed without it, and an "Includes $X tax" note under the total for a tax-included price.
  • The default, brocode, rombie, taut-wad and custom themes, each in dark and light, the --bcc-* properties, appearance and fonts.
  • toAppearance, THEMES and resolveColors.