---
title: "Why iframe form embeds break"
description: "Six failure modes that follow from one fact — an iframe is a separate browsing context — plus the trade-offs of script embeds and native SDK rendering."
order: 27
type: "guide"
topic: "Evaluating tools"
tags:
  - "iframe"
  - "embed"
  - "CSP"
  - "accessibility"
  - "SDK"
updated: "2026-08-06"
---

An iframe is not a component. It's a second browsing context with its own document, its own layout viewport, its own origin, and its own accessibility tree, painted inside a rectangle you reserve for it. Almost every complaint about embedded forms follows from that one fact rather than from any vendor's implementation quality.

That's worth stating plainly, because it means the failures below aren't bugs someone forgot to fix. They're the boundary doing what a boundary does. Knowing which ones you can live with is the whole decision.

## Six ways the boundary shows up

### 1. Either side can refuse the embed

Two different policies have to agree before a frame renders.

The embedded page controls who may embed it, using the CSP [`frame-ancestors`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/frame-ancestors) directive. MDN's description of what happens when the policy doesn't match is short: if any ancestor doesn't match, the load is cancelled. Not a styled error — a cancelled load.

The host page controls what it may embed, using `frame-src` (or the older `child-src`). A security team tightening the host application's CSP is a normal, healthy thing to do, and it will break a third-party form embed that nobody remembered to add to the allowlist.

Neither policy is visible in the form builder. The failure surfaces in production, in the console, usually after someone else's deploy.

### 2. Autofill stops at the boundary

Browsers scope form-filling heuristics to a document. A cross-origin frame is a different document, so the parent page's context — the account the user is signed into, the address they just typed into your checkout — doesn't reach the fields inside the frame. Respondents retype things they've already given you, on a form that from their point of view is part of your product.

There's no header or attribute that fixes this from the outside. The fields have to be in the same document as the rest of the page for the browser to treat them as part of the same form-filling context.

### 3. Height is not automatic

An iframe has a fixed box. It does not grow to fit its content, and the parent cannot measure the child's content across origins. So every embed script solves the same problem the same way: the child measures itself and posts its height to the parent, and the parent resizes the frame.

This works, right up until the height changes for a reason the handshake doesn't observe — conditional logic revealing a section, a validation error appearing, a font loading late, the viewport rotating. Then you get the two classic artifacts: a form cut off at the bottom, or a tall empty gap under a short form. Multi-step forms make it worse, because each step is a different height.

### 4. Popups clip at the frame edge

A dropdown, date picker, or autocomplete list rendered inside the frame cannot paint outside it. The frame is the child document's viewport. When a select near the bottom of a short frame opens, the browser flips it upward or clips it, and on a small screen there may be nowhere for it to go at all.

The same form opened as a full page behaves perfectly, which is why this one is so often missed in review: it only appears at the embedded size, on a narrow viewport, with a field near an edge.

### 5. Attribution thins out

Cross-site state inside a frame is being restricted across all major browsers. MDN's overview of [third-party cookies](https://developer.mozilla.org/en-US/docs/Web/Privacy/Guides/Third-party_cookies) describes Firefox's Total Cookie Protection giving third-party cookies a separate jar per site, and Safari's Intelligent Tracking Prevention on by default. Partitioned storage (CHIPS) is the sanctioned path forward, and it is opt-in and per-site by design.

For a form, the practical consequence is that the analytics identity, session, and campaign context living on your page frequently do not reach the code running inside the frame. Submissions arrive without the attribution that would tell you which campaign produced them, and the gap is inconsistent across browsers, which is worse than being consistently absent.

The workable mitigation is to stop relying on shared browser state and pass context explicitly as parameters or hidden fields — which is a thing you have to design for, on any tool.

### 6. Assistive technology sees a separate document

A frame is a document boundary in the accessibility tree too. Heading order, landmark structure, and focus management inside the frame are separate from the page around it, and moving in and out of an embedded document is a distinct navigation step for screen-reader users.

Jotform documents an instance of this directly: their help center carries an answer titled around screen readers not being able to read a form embedded with the iframe embed code, and points users toward a different embed method ([Jotform help center](https://www.jotform.com/answers/2673463-ada-screen-readers-are-not-able-to-read-the-form-embedded-with-iframe-embed-code)). Cited here because it's the vendor's own documentation of the limitation rather than an outside characterization of it — the underlying constraint isn't specific to their product.

## What iframes are genuinely good for

The boundary is also the feature, and a fair comparison has to say so.

An iframe gives you real isolation: the embedded form can't be restyled by your CSS, can't be broken by your JavaScript, and can't read your DOM. It ships no dependency into your bundle, needs no build step, and works identically in every framework and CMS because it isn't running in your application at all. For a marketing page, a help-center article, a Notion or WordPress page, or anywhere a form needs to be dropped in by someone who doesn't deploy code, that isolation is exactly right.

If your form lives on a page rather than inside a product flow, most of this article is describing problems you'll never have.

## The alternatives, and what each costs

| 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 | All six boundary effects above |
| Script embed | Renders into your page, no frame boundary | A third-party script in your bundle and runtime |
| Native SDK | Real components in your tree, your styles, your context | A dependency you version and upgrade |

**Script embeds** remove the boundary but move the cost into your build and runtime. A vendor script that touches `window` at import time breaks server-side rendering unless it's loaded dynamically and client-side only; bundlers and meta-frameworks each have their own way of tripping over that. The script arrives at a version you don't pin, injects DOM and CSS into your page, and its weight lands on the same page you're optimizing. You've traded a frame boundary for a supply-chain and performance question.

**Native SDK rendering** means the form's fields are real elements in your document: your CSS applies, your design system applies, autofill works because there's only one document, popups position against the real viewport, and the accessibility tree is continuous. The cost is a genuine dependency — a package to install, version, and upgrade, and a schema contract between your app and the form service. It's also the only option on this list that requires a developer, which for some teams is the deciding factor in the other direction.

None of this exempts a native SDK from CSP. It exempts it from *someone else's* CSP: the code runs under your application's policy, which you already control, instead of negotiating between two policies owned by different teams.

## Choosing

Ask where the form lives, not which technology sounds better:

- **On a marketing page, help doc, or CMS page** — hosted link or iframe. The isolation is worth more than the polish.
- **Inside a product flow, behind sign-in, mid-workflow** — native rendering. Autofill, styling, and account context are the whole point, and those are precisely what the boundary costs you.
- **Somewhere in between** — try the iframe first at the real embedded size, on a narrow viewport, with a keyboard and a screen reader. The failure modes above are cheap to reproduce and expensive to discover later.

## How Fillo renders

Fillo's SDK path renders native controls: [`@usefillo/react`](/docs/embed) returns real DOM into your React tree, and [`@usefillo/dom`](/docs/embed) provides a framework-neutral renderer and custom element for Vue, Svelte, Astro, or a plain browser app. Default styles are optional and override-friendly, per-field overrides and fully custom layouts are supported, and validation, uploads, and responses are handled behind the components. Fillo also serves a hosted page when a link is the right answer — that path is a hosted form like any other, and the native-rendering claims above apply to the SDK, not to it.

## Related

- [Quickstart](/docs/embed): Render a form natively in React, Next.js, or any browser app.
- [Styling](/docs/styling): Apply your own CSS, tokens, or design system to the default renderer.
- [Custom UI](/docs/custom-ui): Replace individual fields or build a fully custom layout with headless hooks.
- [In-app feedback](/in-app-feedback): See the native-rendering trade-off in a signed-in product workflow.
- [CORS error](/docs/troubleshooting/cors-error): Diagnose a blocked request from your own origin.
- [Custom layout example](/examples#custom-layout): A fully custom-rendered Fillo form — no frame, your components.
