ページの中に別のページを埋め込む 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 を使う

参考