ページの中に別のページを埋め込む iframe は、地図や動画、外部のフォームなどでよく使われます。iframe を使うときにまず押さえておきたいのは、「iframe の中は、親ページとはまったく別の文書」だということです。親ページで読み込んだ CSS も JavaScript も、iframe の中のページには効きません。
補正スクリプトが iframe の中で効かなかった例
IE6 の時代には、透過 PNG を表示するために、DD_belatedPNG などの補正スクリプトを読み込んでいました (仕組みは IE6 の透過 PNG 対策と、DD_belatedPNG で画像がずれる理由 で紹介しています)。親ページではきちんと直っているのに、iframe で表示した部分だけ PNG が灰色のまま、ということがよくありました。
原因は単純で、補正スクリプトは、自分が読み込まれた文書の中の要素しか扱わないからです。iframe の中のページを直すには、そのページ自身にもスクリプトを読み込む必要がありました。
iframe の中は別の文書
iframe で読み込んだページは、独立した文書として扱われます。
documentとwindowが、それぞれ別にある。親ページのdocument.querySelectorAll()では、iframe の中の要素は見つからない- 親ページの CSS は、iframe の中には効かない。中の見た目は、中のページの CSS だけで決まる
- 親ページで定義した関数や変数は、中のスクリプトからそのままでは呼べない
- DOCTYPE も別なので、中のページだけが互換モードになることもある (レンダリングモードについては DOCTYPE とレンダリングモード を参照)
そのため、iframe で表示するページにも、必要な CSS やスクリプト、<!doctype html> を、ふつうのページと同じように書きます。
親ページと iframe の中が同じオリジン (スキーム・ホスト名・ポート番号の組) なら、contentDocument や contentWindow を通して、中のページを操作できます。中のページからは、window.parent で親ページの window を参照できます。
// 親ページのスクリプト (iframe の中が同じオリジンのときだけ動く)
const frame = document.querySelector("#help-frame");
frame.addEventListener("load", () => {
frame.contentDocument.body.classList.add("is-embedded");
});
オリジンが違うときは postMessage でやり取りする
オリジンが違うページどうしは、同一オリジンポリシーによって、互いの中身を読み書きできません。contentDocument は null になり、window.parent の中の要素に触れようとするとエラーになります。
オリジンをまたいでやり取りしたいときは、postMessage() でメッセージを送ります。次の例では、iframe の中のページが自分の高さを親ページに知らせ、親ページが iframe の高さを合わせています。
// iframe の中のページ (https://widget.example.com)
const observer = new ResizeObserver(() => {
const height = document.documentElement.offsetHeight;
// 送り先のオリジンを指定する ("*" にしない)
window.parent.postMessage({ type: "resize", height }, "https://example.com");
});
observer.observe(document.documentElement);
// 親ページ (https://example.com)
const frame = document.querySelector("#widget-frame");
window.addEventListener("message", (event) => {
// 決まったオリジンの、決まった iframe からのメッセージだけを受け取る
if (event.origin !== "https://widget.example.com") return;
if (event.source !== frame.contentWindow) return;
if (event.data?.type === "resize") {
frame.style.height = `${event.data.height}px`;
}
});
message イベントは、どのページからでも送られてくる可能性があります。受け取る側では、必ず event.origin を確かめてから中身を使ってください。
今の iframe で気をつけること
title属性: スクリーンリーダーなどの支援技術は、iframe をtitleの文字で読み上げる。「駅周辺の地図」「お問い合わせフォーム」のように、中身がわかる名前を付けるloading="lazy": 画面の外にある iframe の読み込みを、近くまでスクロールされるまで遅らせる。ページの下のほうにある地図や動画に向いているsandbox属性: 中のページに許すことを限る。値を空にすると、スクリプトの実行やフォームの送信などがまとめて禁止され、allow-scriptsやallow-formsのように必要なものだけを許可できる。同じオリジンのページにallow-scriptsとallow-same-originを両方与えると、中のスクリプトが sandbox 自体を外せてしまうので、この組み合わせには注意する- 埋め込まれる側の対策: 自分のページを、ほかのサイトの iframe に勝手に埋め込まれたくないときは、Content Security Policy (CSP) の
frame-ancestorsやX-Frame-Optionsヘッダーで禁止する。見えない iframe を重ねてクリックさせる攻撃 (クリックジャッキング) の対策になる
<!-- ページの下のほうに置く地図 -->
<iframe src="https://maps.example.com/embed?place=station"
title="駅周辺の地図" width="600" height="400" loading="lazy"></iframe>
<!-- 信頼できない内容のプレビュー: スクリプトもフォームも動かさない -->
<iframe src="/preview/123" title="投稿のプレビュー" sandbox></iframe>
まとめ
- iframe の中は別の文書。親ページの CSS やスクリプトは効かないので、中のページにも必要なものを読み込む
- 親ページと iframe の中を直接操作できるのは、同じオリジンのときだけ
- オリジンが違うときは
postMessage()を使い、送り先のオリジンの指定とevent.originの確認を必ず行う titleを付け、必要に応じてloading="lazy"やsandboxを使う