Architecture@billing-io/designs v1.6.2

How we think about design
at billing.io

Design is the single most important factor in payment conversion. A checkout that feels broken, slow, or unfamiliar will lose customers — no matter how good the backend is. Every border radius, every color, every animation exists to build trust in the 3–5 seconds a user decides whether to complete their payment.

That’s why we built a unified design system that powers every billing.io surface from a single package. Change one file, publish, and the update propagates to hosted checkouts, invoices, embedded overlays — everything.

Unified Design System

All billing.io payment UI is owned by a single NPM package:@billing-io/designs. The hosted checkout, the hosted invoice, and the embedded overlays all import their components from here. There is no UI duplication.

NPM Package

@billing-io/designs

Single source of truth

checkout/overlay/brand

Hosted

Checkout & Invoice

Direct React import

pay.billing.io/checkout/...

Embedded

billing.js Overlays

iframe → hosted card

js.billing.io/v1/billing.js

Design Site

This Preview App

Direct React import

localhost:3003

The Package

@billing-io/designsis published to NPM and lives on GitHub atbilling-io/designs. It exports three modules:

checkout/

CheckoutCard component, all SVG icons, payment state types, wallet deep-link builders.

overlay/

Overlay constants (dimensions, backdrop, border radius) and the MSG_PREFIX for the iframe bridge.

brand

The canonical “b.” logo SVG, badge builder, and brand constants used in every footer.

How consumers import
// Hosted checkout (Next.js app on pay.billing.io)
import { CheckoutCard } from "@billing-io/designs/checkout";
import type { PaymentDisplayState } from "@billing-io/designs/checkout";

// Rendering the checkout — all visual design is owned by the package.
// The consumer only passes data props.
<CheckoutCard
  status="paying"
  amount="$20.00"
  token="USDT"
  chain="Tron"
  address="TN7hMd7PKEaZLMZe8eQhWpGjEpJJFbGSxE"
  merchantName="your-app.com"
/>

Integration Modes

There are two fundamentally different ways a merchant’s customer sees a billing.io checkout. Both render the exact same CheckoutCard, but the delivery mechanism is different.

Hosted Checkout & Invoice

Direct import

The merchant creates a checkout session via our API and redirects the customer topay.billing.io/checkout/co_.... This is a Next.js app that directly imports CheckoutCard from the package. Same for invoices.

Because it’s a direct React import, the hosted checkout gets type safety, tree-shaking, and instant hot reloads during development. When we publish a new version of @billing-io/designs, we bump the dependency in the hosted app, redeploy, and every live checkout page is updated.

Embedded Overlays (billing.js)

iframe

The merchant drops a <script> tag on their site that loads billing.js. When they call billing.openCheckout(), the script creates an overlay (centered modal, side panel, bottom sheet, or fullscreen) with an iframe inside that loads the hosted checkout page.

The iframe and the parent page communicate via postMessage. The checkout sends status updates (billing:status,billing:resize) so the overlay can react to payment state changes and auto-size.

Why an iframe?

  • CSS isolation. The merchant’s site CSS cannot leak into the checkout UI. No broken layouts, no style conflicts, no surprises.
  • Security boundary. The checkout runs in its own origin. The merchant’s JavaScript cannot tamper with the payment address, amount, or state. This is critical for a payment flow.
  • Zero-dependency for the merchant. No React, no Tailwind, no build step. Just a script tag. The iframe handles all rendering internally.
  • Same code, same updates. The iframe loads the same hosted checkout page. When we update the design, embedded users get the update too — no NPM install required on their end.
How embedded overlays work under the hood
// billing.js (simplified) — what happens when a merchant calls openCheckout()

billing.openCheckout({
  checkoutId: "co_abc123",
  variation: "centered",   // or "panel", "bottom", "fullscreen"
});

// 1. billing.js creates a backdrop + overlay container on the merchant's page
// 2. It injects an <iframe> pointing to the hosted checkout:
//      src="https://pay.billing.io/checkout/co_abc123?embed=1&fill=1"
// 3. The checkout page renders CheckoutCard with isEmbed=true
// 4. The iframe sends postMessage events back to billing.js:
//      "billing:loaded"  → overlay fades in
//      "billing:status"  → { status: "detected" } → overlay reacts
//      "billing:resize"  → { height: 640 } → bottom sheet auto-sizes
// 5. On success, billing.js calls the merchant's onSuccess callback

Rapid A/B Testing

Because every surface imports from the same package, we can test design changes across the entire product with a single publish cycle:

  1. 1Change the component in billing-designs/src/checkout/checkout-card.tsx
  2. 2Preview it locally on this design site (all modes, all states, all devices)
  3. 3Bump the version and npm publish
  4. 4Update the dependency in the hosted app and redeploy — every live checkout, invoice, and embedded overlay is updated instantly

This is how we just shipped the minimal white wallet buttons (MM5 + TW5). One file change in the design package, one publish to @billing-io/designs@1.6.2, and the new style is live across every integration mode.

Package Structure

The package is intentionally small and focused. No external UI dependencies — all icons are inline SVGs, all styling is Tailwind CSS.

@billing-io/designs
src/
├── checkout/
│   ├── checkout-card.tsx    ← The checkout UI (single source of truth)
│   ├── icons.tsx            ← All SVG icons (Lock, Shield, MetaMask, TrustWallet, etc.)
│   ├── types.ts             ← CheckoutCardProps, PaymentDisplayState
│   └── index.ts             ← Public exports
├── overlay/
│   ├── constants.ts         ← Overlay dimensions (centered, panel, bottom sheet, fullscreen)
│   └── index.ts             ← MSG_PREFIX for iframe postMessage bridge
├── brand.ts                 ← "b." logo SVG paths, badge builder
├── index.ts                 ← Root exports
└── billing-js/
    └── billing.js           ← The drop-in script for embedded overlays

Coming Soon
Merchant Branding & Customization

Roadmap

Right now the checkout modal uses a fixed design. In the next version, we’re opening it up so merchants can fully customize the checkout experience to match their brand:

Custom Logo

Upload a logo that appears in the checkout header instead of the default merchant name text. SVG, PNG, or ICO.

Custom Domain

Serve the hosted checkout from the merchant’s own domain, e.g. pay.your-app.com instead of pay.billing.io.

Brand Colors

Set a primary accent color that tints buttons, badges, and the copy-address CTA. The design system will generate accessible contrast ratios automatically.

Custom Copy

Override default strings like “via billing.io secure checkout” with merchant-specific text. Useful for white-label deployments.

Technically, this will work by extending the CheckoutCardProps interface with an optional branding object. The merchant configures their branding in the dashboard, it gets stored on the checkout session, and the hosted app passes it to CheckoutCard as props. Because embedded checkouts use an iframe to the hosted page, merchants using billing.js get branding for free — no code changes on their side.

Future: CheckoutCard with branding (draft)
// What the branding prop will look like
<CheckoutCard
  status="paying"
  amount="$20.00"
  token="USDT"
  chain="Tron"
  address="TN7hMd7PKEaZLMZe8eQhWpGjEpJJFbGSxE"
  merchantName="Acme Inc."
  branding={{
    logo: "https://cdn.your-app.com/logo.svg",
    accentColor: "#6366F1",              // indigo-500
    headerText: "Pay securely with Acme",
    showPoweredBy: true,                 // "Powered by billing.io" in footer
  }}
/>

// The same props get passed through to embedded checkouts automatically.
// Merchant configures branding once in the dashboard → stored on the
// checkout session → rendered by CheckoutCard → works in hosted AND
// embedded modes with zero extra work.