Skip to content

Iframe Embedding

Iframe mode runs the TokenFlight UI in a separate page, then connects it back to your app with postMessage.

Most teams should start with the normal <tokenflight-widget>. Use iframe mode when you need stronger isolation, a stricter CSP setup, or a self-hosted widget receiver.

The simple model

There are two pages:

Host page

This is your app. It mounts the iframe, owns the wallet adapter, receives callbacks, and opens card popups.

Receiver page

This page runs inside the iframe. It renders the TokenFlight UI, calls TokenFlight APIs, and asks the host to sign wallet actions.

By default, the receiver page is hosted by TokenFlight at:

txt
https://embed.tokenflight.ai/widget

You can also host the receiver yourself and point iframeSrc at your own URL.

When to use iframe mode

Use iframe mode when one of these is true:

  • Your app needs to isolate the widget from app CSS, globals, or framework runtime.
  • Your CSP allows trusted frames but you want to reduce direct widget code in the host page.
  • You need the host page to open card payment popups from the top-level window.
  • You want to self-host the receiver page on your own origin.

Prefer the normal widget when you do not need iframe isolation. It is simpler and has fewer moving parts.

Choose an API

Iframe mode has two host-side APIs.

APIUse it when
<tokenflight-iframe-widget>You want a drop-in custom element, similar to <tokenflight-widget>
createTokenFlightBridge()You want framework lifecycle control, runtime config updates, or direct event subscriptions

Both APIs load the same receiver page. Both support iframeSrc for self-hosting.

Quick start: hosted receiver

This path uses the default receiver hosted by TokenFlight.

1. Install the package

sh
pnpm add @tokenflight/swap

For crypto or mixed crypto + card flows, also install and create a wallet adapter. See Why wallet adapters.

2. Register the iframe custom element

In this snippet, walletAdapter comes from your wallet setup file. For example, it can be an AppKit, wagmi, ethers, or custom wallet adapter.

ts
import { registerIframeElement } from "@tokenflight/swap/iframe";
import { walletAdapter } from "./wallet";

registerIframeElement({
  walletAdapter,
});

If you only enable card payments, you can omit walletAdapter.

3. Add the element

html
<tokenflight-iframe-widget
  theme="dark"
  trade-type="EXACT_OUTPUT"
  amount="100"
  to-token="eip155:8453:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
></tokenflight-iframe-widget>

The attributes match <tokenflight-widget>. See HTML Attributes for the full list.

4. Use card-only mode without a wallet

If the page only accepts card payments, lock the payment methods to card:

html
<tokenflight-iframe-widget
  theme="light"
  trade-type="EXACT_OUTPUT"
  amount="100"
  methods='["card"]'
  default-pay-method="card"
  to-token="eip155:8453:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
></tokenflight-iframe-widget>

Do not offer crypto methods unless the host page provides a wallet adapter.

Imperative host setup

Use createTokenFlightBridge() when your framework wants to create and destroy the iframe in code.

The walletAdapter is the same adapter you would pass to the normal widget.

ts
import { createTokenFlightBridge } from "@tokenflight/swap/bridge";
import { walletAdapter } from "./wallet";

const bridge = createTokenFlightBridge({
  container: "#tokenflight",
  wallet: walletAdapter,
  config: {
    theme: "dark",
    tradeType: "EXACT_OUTPUT",
    amount: "100",
    toToken: {
      chainId: 8453,
      address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
    },
  },
  onReady: (version) => {
    console.log("TokenFlight iframe ready", version);
  },
});

bridge.on("swapComplete", (event) => {
  console.log("Order complete", event.orderId);
});

bridge.on("error", (error) => {
  console.error("TokenFlight error", error.message);
});

bridge.setConfig({ amount: "200" });

// Later, when your page unmounts:
bridge.destroy();

Available bridge events:

EventMeaning
readyThe receiver sent its ready message
swapCompleteCrypto or receive flow completed
errorThe widget reported an error
fiatOrderCreatedA card order was created
fiatOrderCompletedA card order reached a terminal state
resizeThe iframe reported a new content height

What happens during a payment

The host and receiver split the work:

  1. The host sends config and wallet state to the receiver.
  2. The receiver renders the TokenFlight UI.
  3. If the user pays with crypto, the receiver asks the host wallet adapter to sign.
  4. If the user pays by card, the receiver asks the host to open the popup.
  5. The receiver tracks the order and sends events back to the host.

You usually do not need to handle these protocol messages yourself. The bridge does it.

Self-host the receiver

Self-hosting means you serve the iframe receiver page from your own domain instead of using https://embed.tokenflight.ai/widget.

Self-host when you need one of these:

  • Same-origin iframe hosting for your app.
  • A pinned receiver version that only changes when you deploy it.
  • A custom domain in your CSP.
  • A forked receiver build.

The host page still uses registerIframeElement() or createTokenFlightBridge(). The only change is iframeSrc.

1. Create a receiver app

Create a small static app. Vite is enough:

sh
pnpm create vite tokenflight-receiver -- --template vanilla-ts
cd tokenflight-receiver
pnpm add @tokenflight/swap

Use the same @tokenflight/swap version in the host app and the receiver app.

2. Add the receiver HTML

html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>TokenFlight Receiver</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>

3. Mount the receiver

ts
import { mountIframeReceiver } from "@tokenflight/swap/iframe-entry";
import "./global.css";

mountIframeReceiver();
css
html,
body,
#root {
  margin: 0;
  min-width: 0;
  background: transparent;
}

mountIframeReceiver() wires up the postMessage bridge, theme variables, widget render, and resize reporting.

4. Configure the deploy path

If the receiver will be served at /tf-widget/, set Vite base to the same path:

ts
import { defineConfig } from "vite";

export default defineConfig({
  base: "/tf-widget/",
});

Build and deploy the static output:

sh
pnpm build

After deploy, this URL should load:

txt
https://your-domain.com/tf-widget/

If you open it directly in a browser, it shows a standalone preview. Inside your app, it connects to the host bridge.

5. Point the host to your receiver

For the custom element:

ts
import { registerIframeElement } from "@tokenflight/swap/iframe";
import { walletAdapter } from "./wallet";

registerIframeElement({
  iframeSrc: "https://your-domain.com/tf-widget/",
  walletAdapter,
});

For the imperative bridge:

ts
import { createTokenFlightBridge } from "@tokenflight/swap/bridge";
import { walletAdapter } from "./wallet";

const bridge = createTokenFlightBridge({
  container: "#tokenflight",
  iframeSrc: "https://your-domain.com/tf-widget/",
  wallet: walletAdapter,
  config: {
    tradeType: "EXACT_OUTPUT",
    amount: "100",
    toToken: "eip155:8453:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  },
});

If the receiver is served from the same site as your app, you can use a relative path:

ts
registerIframeElement({
  iframeSrc: "/tf-widget/",
  walletAdapter,
});

Self-hosting checklist

Before shipping a self-hosted receiver:

  • Host and receiver use the same @tokenflight/swap version.
  • The receiver URL works over HTTPS.
  • The host CSP allows the receiver in frame-src.
  • The receiver CSP allows your app in frame-ancestors.
  • The receiver response does not set X-Frame-Options: DENY.
  • The host passes a wallet adapter for crypto or mixed flows.
  • Card popup flows are tested in the real browser, not only in unit tests.

Example CSP for the host page:

txt
Content-Security-Policy:
  frame-src https://your-domain.com;

Example CSP for the receiver page:

txt
Content-Security-Policy:
  frame-ancestors https://your-app.com;
  connect-src https://api.hyperstream.dev https://fiat.hyperstream.dev;

Replace the domains with your real app, receiver, API, and fiat endpoints.

Styling

The custom element draws the outer frame on the host page. The iframe content stays flat and transparent, so you do not get double borders.

Style the host shell with ::part(container):

css
tokenflight-iframe-widget::part(container) {
  border-radius: 24px;
}

With the imperative bridge, style your own container:

css
#tokenflight {
  width: 100%;
  max-width: 420px;
  border: 1px solid rgba(148, 163, 184, 0.3);
  border-radius: 24px;
  overflow: hidden;
}

You can also send TokenFlight CSS variables:

ts
bridge.setStyle({
  vars: {
    "--tf-primary": "#2563eb",
  },
});

See Theming for the variable list.

Common mistakes

ProblemFix
The crypto flow cannot connect a walletPass walletAdapter on the host page
Card popup is blockedLet the bridge open the popup from the host window during the user click
Self-hosted receiver never becomes readyCheck iframeSrc, CSP, and that host/receiver package versions match
The iframe has double bordersStyle the host shell, not the receiver page
The iframe height is wrongDo not force a fixed iframe height; let the bridge handle resize events

Security notes

  • The host validates messages against the receiver origin derived from iframeSrc.
  • The host also checks that messages come from the iframe it created.
  • The receiver validates messages from the host origin.
  • Raw CSS overrides are size-limited and @import is stripped.
  • The iframe uses allow="camera;microphone;payment;clipboard-write" by default. Trim this only if your integration does not need those permissions.

Next steps