So, embedding stuff from another domain. Payment forms, login widgets, micro-frontends, that kind of thing. If you've done it, you know it's a bit of a pain: postMessage everywhere, origin checks, some janky handshake, and a bunch of message types that slowly drift out of sync between the two apps.
Zoid sorted a lot of this out ages ago, but I wanted something a bit more modern. I wanted types that actually carry across the iframe boundary, zero dependencies and a proper ESM build. So I built ForgeFrame.
How it works
There are two sides to every setup:
- Consumer: the outer app, which renders the iframe or popup and passes props in.
- Host: the app inside the frame, which reads those props.
You define the component once and import it on both sides:
// payment-form.ts
import { create, prop } from 'forgeframe';
export const PaymentForm = create({
tag: 'payment-form',
url: 'https://pay.example.com/checkout',
dimensions: { width: 400, height: 300 },
props: {
amount: prop.number(),
onSuccess: prop.function<(txn: { transactionId: string }) => void>(),
},
});
The consumer renders it, and yep, you can pass functions:
const payment = PaymentForm({
amount: 99.99,
onSuccess: (txn) => console.log('Payment complete:', txn),
});
await payment.render('#payment-container');
The host gets fully typed props, with no second interface to keep in sync:
import { initHost } from 'forgeframe';
import { PaymentForm } from './payment-form';
const host = initHost(PaymentForm, {
consumerOrigins: ['https://shop.example.com'],
});
if (host) {
const { amount, onSuccess } = await host.ready;
// do your UI thing, then:
await onSuccess({ transactionId: 'TXN_123' });
await host.close();
}
That's pretty much it.
The good bits
- Types come straight from the schema, so if you forget a required prop it just won't compile.
- You can pass callbacks across origins, and the host calls them like normal functions.
- Origin allow-listing is built in and required, so you can't accidentally leave the door open.
- It works with iframes or popups, and you get typed lifecycle events like
rendered,closeanderror. - The host can expose typed methods back to the consumer.
- There's React support if you want it.
- It has zero runtime dependencies.
It's already running in production at Tyro Health, which is nice.
Give it a go
npm install forgeframe
Repo's here: https://github.com/jshsmth/forgeframe
If you're on Zoid or have your own postMessage setup, I'd genuinely love to hear what you think, especially about anything weird you've run into. Issues, PRs and stars are all appreciated 🙌
Top comments (0)