Guide
How to Create an Embeddable Widget for Your SaaS
Short answer: pick one feature of your product that works on its own, serve it as a page built to live inside an iframe, and make that page adapt to whatever box it's given. The host passes its look in the URL, the widget reports its own height, and sign-in, payment and usage limits all happen inside the frame, so the host adds one tag and one listener and never touches your code.
Building the feature is the easy half. The hard half is that it has to run inside a product you've never seen, at a width you don't control, in a design that isn't yours, for users who aren't signed in to you. Here's how to build for that.
Which feature should become the widget?
One feature, not your product. The test is whether a user can get the whole job done inside a box on somebody else's screen. A scheduling page passes. An AI rewrite of a headline passes. A reporting dashboard that needs your onboarding, your data import and three settings pages does not.
Three questions that sort it fast:
- Does it finish a job in one sitting? Input in, result out, no detour through your app.
- Does it make sense next to the host's product? Same user, a step before or after what the host already does.
- Can you charge for it on its own? If the widget is a teaser for your real product, hosts can tell, and so can their users.
If you're not sure what kind of integration you're building, the three types of SaaS integration helps: a widget is a capability integration, your function running inside their product, not a logo in their footer.
Iframe, web component or script snippet?
| Approach | How it ships | Inside someone else's product |
|---|---|---|
| Iframe | The host adds one <iframe> pointing at your embed page | Isolated both ways. Your CSS can't leak out, theirs can't leak in, neither side can read the other's data |
| Web component | The host loads your script and drops in a custom element | Shadow DOM isolates styles, but your code runs on their page with their permissions |
| Script snippet | The host pastes a script that injects your UI | Shares everything. Fine on your own marketing site, a security review waiting to happen in a third party's app |
For a widget other companies embed, an iframe is the safe default. The isolation runs both ways and that's the point: the host's security person doesn't have to read your JavaScript, and a CSS reset on their side can't wreck your layout. What you give up (direct access to the page) you get back with a small, explicit set of messages, which is better anyway.
How do you build an embeddable widget in React?
Build it as its own small React app, not as a component you hand to the host. Give it a route on your own domain (for example /embed) with its own entry point and bundle, and serve that page into the host's iframe. The host never installs your React, so there is nothing to clash with their React version, their router or their CSS, and you ship updates without anyone redeploying.
Inside that app the work is the same as any embed: read the design settings from the URL once and turn them into CSS variables, wrap the root in a ResizeObserver that posts its height to the parent, and keep the signed-in state inside the frame. If a host insists on a component, publish a thin wrapper that renders the iframe and wires the resize listener. The React stays on your side either way.
How should an embeddable widget size itself?
To its container, never to the viewport. The same widget will sit in a 300px sidebar card on one host, a full page on another, and a modal opened from a button on a third. So:
- Use container queries on the widget's own width, from about 280px up to 1200px. Tighter spacing below roughly 420px, wider layouts above 640px, no horizontal scroll, nothing fixed-width.
- Support three placements: a card inside one of the host's screens, a full page of its own, and a modal the host opens from its own button. A full page should actually use the width, not sit in a narrow column.
- Hug your content. Never
height: 100vh, never a fixed height.
Then report the height. Put a ResizeObserver on the widget's outermost element and post the height to the parent every time it changes:
window.parent.postMessage({ type: "ordana-widget:resize", height }, "*");The host adds one listener that sets the iframe's height. Keep the message name identical across every widget you ship, so a host that already listens for one gets the next for free. For hosts that want to size the box themselves, support a fit=fill mode that fills the box and scrolls inside it.
How does the widget match the host's design?
The host passes its look in the embed URL and you map each parameter onto a CSS variable. The set that covers almost every product:
theme(light or dark),accent,accent_text,bg,surface,text,muted,borderradius(0 to 24px),fontandheading_font(Google Fonts families, loaded on demand)density(compact, comfortable or spacious),font_size(12 to 18px),button(solid or outline)frame=noneto drop your own border and shadow when the host's card already frames it, while keeping your background, so your text never lands on a colour you didn't choose
The rule that makes this work: the host styles the frame's inputs, never its insides. If a host can reach in and restyle your widget, every host update can break it and you'll support a hundred forks of your own UI.
What does the embed page need to allow?
Three things hosts trip over first. The embed page must allow framing by any site (Content-Security-Policy: frame-ancestors *, and no X-Frame-Options header), it must load for a logged-out visitor, and it must work inside an iframe on a phone. Check all three before you send a single link.
Any data that moves between the host page and your widget goes through postMessage to an exact origin, never "*" for anything carrying data, and you accept messages only from the origins you expect, keeping only the keys you declared.
Where do sign-in and payment happen?
Inside the widget. The host's user should be able to try it, hit the paywall, sign up and pay without ever leaving the host's screen. Two details matter more than they look:
- After sign-in, update in place. No reload of the host's page, no lost input. The user lands exactly where they were, now signed in.
- Say who's who. The user is inside one company's product using another company's tool. A small "What is this?" panel naming both, and who handles sign-in and payment, saves a lot of confused support tickets.
How do you meter free use?
On your server, before every use. A widget with a free tier and an AI call behind it is a cost line running in someone else's product. So before your server runs the expensive part, it asks: does this user, in this host, still have free uses or a paid plan? Only a yes runs the call.
Capping free use per host is the guard that matters. One host with a big audience can otherwise spend your month's AI budget in an afternoon. Count a use when it's consumed, not when the widget loads, or you'll bill people for opening a page.
How do you change a widget after hosts depend on it?
Like an API, because it is one. Version the embed, keep a dated change log, and give notice before anything that breaks a host: a removed URL parameter, a renamed message, a different height behaviour. The same discipline as packaging your API for partners, applied to a frame. Commercial changes need notice too: a host that built around your free tier shouldn't wake up to a paywall.
Why does the preview have to be public?
Because a host decides by clicking. Build a clickable preview page that shows the widget in each placement, with a small switcher for a few looks (default, dark, a different font and square corners) so a host sees it take on their style. Then put it on your real production domain, logged out. Localhost doesn't count, and most preview deployments sit behind a login, so a host opening the link sees a sign-in screen instead of your widget.
Test it the way a host will run it: a plain HTML page that frames the embed with the resize listener, at 280, 390, 768 and 1200px, in a dark theme with a serif font, while you drag the window narrower and wider. No empty block under it, no inner scrollbar, nothing cut off.
How this works on Ordana
You don't write any of this from a blank file. Ordana gives you one prompt for your own coding agent (Claude Code, Codex, Cursor or similar). The agent reads your code, then interviews you: about eight questions, each with its own recommendation, so you can answer "go with that". It designs the widget inside a standard frame, builds the clickable preview in your codebase, and returns one JSON block you paste into Ordana. Ordana never reads your repository.
Ordana runs the sign-in and checkout inside the widget, checks entitlements before each use, and pays out automatically: you set the price, the free uses and the share the host keeps (10 to 90%), and Ordana's fee is 10% of each sale, taken from your share. Pricing, listing and finding hosts are covered in how to sell a SaaS feature as a widget; the host's side is in how to embed a third-party widget in your SaaS.
Frequently asked questions
Should an embeddable widget be an iframe, a web component or a script?
For a widget other companies put inside their product, an iframe is the safe default. It isolates both ways: the host's CSS and scripts can't break your widget, and your code can't read or break the host's page. A web component or script snippet shares the host's page, which is fine for your own site and risky inside someone else's.
How does an iframe widget match the host's design?
The host passes design settings in the embed URL (accent colour, background, text colour, corner radius, font, density, font size, heading font, button style) and the widget turns them into CSS variables. The host never restyles the inside of the frame, so the widget looks native without the host touching your code.
How does an iframe widget get the right height?
The widget measures its own content with a ResizeObserver and posts its height to the parent page with postMessage every time it changes. The host adds one listener that sets the iframe's height. Never use a fixed height or 100vh inside an embed.
How do I stop free users of my widget from running up AI costs?
Meter it on your server. Before each use, your server asks whether this user, in this host product, still has free uses or a paid plan, and only then runs the expensive call. Capping free use per host means one busy host can't burn through your budget.
How do I build an embeddable widget in React?
Build it as its own small React app with its own route and bundle on your domain, and serve that page into the host's product in an iframe. The host never installs your React, so versions, routers and CSS can't clash, and you ship updates without them redeploying. If a host wants a component, give them a thin wrapper that renders the iframe.
Can I test my widget on a preview deployment before listing it?
Only if the preview is publicly reachable. Most preview deployments sit behind a login, so a host can't open them. Ship the preview page to your real production domain, logged out, and share that link.
On Ordana, a SaaS lists one feature as a widget with its terms set, and other products add it to theirs. List your SaaS as a widget, or browse every guide in widget integrations.
Related reading:
- 10 Embeddable Widget Examples (and What They Get Right) — the patterns the best embeds share.
- How to Price an Embeddable Widget — free uses, subscriptions, credits or pay per use.
- What Is a Widget Integration? — the definition, and why it converts better than a link.