An iframe embed puts a second document inside your page, with its own origin, viewport, and accessibility tree. This page covers the six ways that boundary breaks an embedded form, and when a script embed or a native SDK fits better.
Six ways the boundary shows up
1. Either side can refuse the embed
Two policies must agree before a frame renders. The embedded page decides who may embed it with the CSP frame-ancestors directive. If an ancestor doesn't match, the browser cancels the load.
The host page decides what it may embed with frame-src. A security team that tightens your app's CSP will break any form embed nobody added to the allowlist. Neither policy shows up in the form builder, so the failure appears in the console after someone else's deploy.
2. Your app's context stops at the frame
A cross-origin frame can't read your page. The signed-in account, the plan, and the values your app already knows don't reach the fields inside it. Respondents retype things they already gave you, on a form that looks like part of your product. The only bridge is what you pass through the URL.
3. Height isn't automatic
An iframe has a fixed box, and the parent can't measure a cross-origin child. Embed scripts work around this: the child posts its height, and the parent resizes the frame.
That breaks when height changes for a reason the handshake misses. Conditional logic reveals a section, a validation error appears, a font loads late, or the screen rotates. You get a form cut off at the bottom or a gap under a short one. Multi-step forms make it worse because each step has a different height.
4. Popups clip at the frame edge
A dropdown, date picker, or autocomplete list inside the frame can't paint outside it. A select near the bottom of a short frame flips upward or clips. On a small screen it may have nowhere to go.
The same form opened as a full page works, which is why review often misses this. It only appears at the embedded size, on a narrow viewport, near an edge.
5. Attribution thins out
Browsers restrict cross-site state inside frames. MDN's overview of third-party cookies describes Firefox giving each site a separate cookie jar and Safari's tracking prevention being on by default. Partitioned cookies (CHIPS) are opt-in and per site.
For a form, the analytics identity and campaign context on your page often don't reach the frame. Submissions arrive without the source that produced them, and the gap differs by browser. Pass that context explicitly as URL parameters or hidden fields, whatever tool you use.
6. Assistive technology sees a separate document
A frame is a document boundary in the accessibility tree. Heading order, landmarks, and focus inside it are separate from the page around it. Moving in and out is an extra navigation step for screen-reader users.
Jotform's help center has an answer about screen readers failing to read a form embedded with its iframe code, and it points users to a different embed method (Jotform help center). The constraint isn't specific to Jotform.
When an iframe is the right call
The boundary is also isolation. Your CSS can't restyle the form, your JavaScript can't break it, and it can't read your DOM. It adds nothing to your bundle, needs no build step, and works in every CMS. On a marketing page, a help article, or a page edited by someone who doesn't deploy code, that isolation is worth more than the polish.
The alternatives
| Approach | You get | You pay |
|---|---|---|
| Hosted link | Zero integration, nothing to break in your app | The respondent leaves your product |
| Iframe embed | Isolation, no dependency, works anywhere | The six effects above |
| Script embed | Renders into your page, no frame | A third-party script in your bundle and runtime |
| Native SDK | Real components in your tree, with your styles and context | A dependency you version and upgrade |
A script embed removes the frame but moves the cost into your build. A vendor script that touches window at import breaks server rendering unless you load it client-side only. It often arrives at a version you don't pin and injects its own DOM and CSS into your page.
A native SDK renders real elements in your document. Your CSS and design system apply, popups position against the real viewport, and the accessibility tree is continuous. The cost is a package to install and upgrade, and a developer to wire it up. The code runs under your app's CSP, which you already control.
Choose by where the form lives
- A marketing page, help doc, or CMS page: use a hosted link or an iframe.
- Inside a product flow, behind sign-in: render natively. Styling and account context are the point.
- Somewhere in between: test the iframe at its real size, on a narrow viewport, with a keyboard and a screen reader.
How Fillo renders
@usefillo/react renders native controls into your React tree. @usefillo/dom does the same for Vue, Svelte, Astro, or plain browser code. Default styles are optional, and you can replace any field or the whole layout. Fillo also serves a hosted page when a link is the right answer.
Related
- Custom layout example: A Fillo form rendered with custom components and no frame.
- Quickstart: Render a form natively in React, Next.js, or any browser app.
- Styling: Apply your CSS, tokens, or design system to the default renderer.
- Custom UI: Replace fields or build a fully custom layout with headless hooks.