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:
https://embed.tokenflight.ai/widgetYou 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.
| API | Use 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
pnpm add @tokenflight/swapFor 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.
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
<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:
<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.
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:
| Event | Meaning |
|---|---|
ready | The receiver sent its ready message |
swapComplete | Crypto or receive flow completed |
error | The widget reported an error |
fiatOrderCreated | A card order was created |
fiatOrderCompleted | A card order reached a terminal state |
resize | The iframe reported a new content height |
What happens during a payment
The host and receiver split the work:
- The host sends config and wallet state to the receiver.
- The receiver renders the TokenFlight UI.
- If the user pays with crypto, the receiver asks the host wallet adapter to sign.
- If the user pays by card, the receiver asks the host to open the popup.
- 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:
pnpm create vite tokenflight-receiver -- --template vanilla-ts
cd tokenflight-receiver
pnpm add @tokenflight/swapUse the same @tokenflight/swap version in the host app and the receiver app.
2. Add the receiver 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
import { mountIframeReceiver } from "@tokenflight/swap/iframe-entry";
import "./global.css";
mountIframeReceiver();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:
import { defineConfig } from "vite";
export default defineConfig({
base: "/tf-widget/",
});Build and deploy the static output:
pnpm buildAfter deploy, this URL should load:
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:
import { registerIframeElement } from "@tokenflight/swap/iframe";
import { walletAdapter } from "./wallet";
registerIframeElement({
iframeSrc: "https://your-domain.com/tf-widget/",
walletAdapter,
});For the imperative bridge:
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:
registerIframeElement({
iframeSrc: "/tf-widget/",
walletAdapter,
});Self-hosting checklist
Before shipping a self-hosted receiver:
- Host and receiver use the same
@tokenflight/swapversion. - 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:
Content-Security-Policy:
frame-src https://your-domain.com;Example CSP for the receiver page:
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):
tokenflight-iframe-widget::part(container) {
border-radius: 24px;
}With the imperative bridge, style your own container:
#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:
bridge.setStyle({
vars: {
"--tf-primary": "#2563eb",
},
});See Theming for the variable list.
Common mistakes
| Problem | Fix |
|---|---|
| The crypto flow cannot connect a wallet | Pass walletAdapter on the host page |
| Card popup is blocked | Let the bridge open the popup from the host window during the user click |
| Self-hosted receiver never becomes ready | Check iframeSrc, CSP, and that host/receiver package versions match |
| The iframe has double borders | Style the host shell, not the receiver page |
| The iframe height is wrong | Do 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
@importis 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
- Why wallet adapters - choose the wallet adapter for crypto flows.
- CSP - configure
frame-src,connect-src, and related directives. - Events & Callbacks - handle success and error events.