The grey "refused to connect" box means the page inside your iframe told the browser not to be framed. It does that with an X-Frame-Options response header (DENY or SAMEORIGIN) or a Content-Security-Policy frame-ancestors rule. Only the framed site's server can lift it. Paste its headers below to see which rule blocks you.
| Header on the framed page | Who may frame it |
|---|---|
| X-Frame-Options: DENY | Nobody, not even the same site |
| X-Frame-Options: SAMEORIGIN | Only pages on the same origin |
| X-Frame-Options: ALLOW-FROM ... | Obsolete, ignored by modern browsers |
| Content-Security-Policy: frame-ancestors 'none' | Nobody |
| Content-Security-Policy: frame-ancestors 'self' https://a.example | The same origin plus the listed sites |
| Neither header | Anyone |
Updated
This error is not a network failure and nothing is wrong with your iframe tag. The browser fetched the page, read a header that forbids framing, and replaced the content with an error. Clickjacking protection is the reason: a page that can be framed by anyone can be overlaid with invisible buttons. The fix depends on whose server sent the header.
Paste the response headers of the page inside the iframe. Everything runs in this page; nothing is fetched.
Response headers of the framed page
HTTP/2 200
content-type: text/html; charset=utf-8
x-frame-options: SAMEORIGIN
content-security-policy: default-src 'self'; frame-ancestors 'self' https://partner.exampleOrigin of the page that holds the iframe: https://shop.example
Origin of the framed page: https://docs.example
Restore the sample headers
Each example allows the same site plus one partner. Replace https://partner.example with the origin that should be able to frame you, or use 'none' to block everyone.
always keeps the header on error pages too. Remove any older X-Frame-Options line so the two rules do not drift apart.
add_header Content-Security-Policy "frame-ancestors 'self' https://partner.example" always;Needs mod_headers. Header always set replaces any earlier value of the same header.
Header always set Content-Security-Policy "frame-ancestors 'self' https://partner.example"Helmet sets X-Frame-Options to SAMEORIGIN by default. Turning that off and setting frame-ancestors keeps one source of truth.
import express from "express";
import helmet from "helmet";
const app = express();
app.use(
helmet({
xFrameOptions: false,
contentSecurityPolicy: {
directives: { frameAncestors: ["'self'", "https://partner.example"] },
},
}),
);headers() adds response headers to every matching route. Hosting platforms may add their own, so check the live response afterwards.
module.exports = {
async headers() {
return [
{
source: "/:path*",
headers: [
{ key: "Content-Security-Policy", value: "frame-ancestors 'self' https://partner.example" },
],
},
];
},
};
Browsers show the same grey box whichever rule fired, but the console names it. Look for a message mentioning X-Frame-Options or frame-ancestors, then open the Network panel to see the exact value the server sent.
Redirects matter too. If the iframe's address redirects to a login page, the header that counts is on the final response, which is often stricter than the page you expected.
Decide who should be allowed to frame the page, then express that once, ideally as a frame-ancestors directive in your Content-Security-Policy. If a framework or security middleware adds X-Frame-Options by default, change its setting rather than adding a second conflicting header.
After deploying, clear any CDN cache and check the live response headers again, because many hosts add their own security headers at the edge.
A site that blocks framing has decided it does not want to appear inside other pages. A link, a screenshot with a link, or the site's own embed widget are the honest alternatives.
Proxying another site's pages through your server to strip the header usually breaks its logins and may break its terms of use, so it is not a fix worth shipping.
No. The rule is enforced by the visitor's browser on behalf of the framed site, and no attribute on your iframe can override it. The only routes are changing the framed site's headers, using an embed URL the site provides for framing, or linking out.
Check for X-Frame-Options: DENY, or frame-ancestors 'none'. Both block every parent, including pages on the same domain. Switch to SAMEORIGIN or frame-ancestors 'self'. Remember that a different subdomain or port is a different origin.
Use frame-ancestors, because it can list several allowed parents and wins whenever both are present. If you also send X-Frame-Options, keep it consistent with the CSP rule so the next person reading the config is not misled.
The ordinary watch or place page usually sends a framing header, while the service publishes a separate embed address meant for iframes, often found under Share then Embed. Our embed code generator converts supported links into those embed addresses for you.
Yes. If your iframe sits inside another iframe, every ancestor up to the top page must match the policy, not only the direct parent.
Related: iframe sandbox tokens, iframe generator, Embed code generator, HTML iframe and video guide, CORS explained.
Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.