[{"data":1,"prerenderedAt":1890},["ShallowReactive",2],{"en-post-\u002Fen\u002Fiframe-auto-height-code-preview":3},{"id":4,"title":5,"body":6,"description":1878,"extension":1879,"meta":1880,"navigation":824,"path":1885,"seo":1886,"sitemap":1887,"stem":1888,"__hash__":1889},"blogEn\u002Fen\u002Fiframe-auto-height-code-preview.md","Auto-Resize an iframe to Its Content: A Code Preview with srcdoc and postMessage",{"type":7,"value":8,"toc":1858},"minimark",[9,14,27,32,35,127,131,146,179,184,192,199,202,206,216,219,294,303,316,318,322,329,521,540,597,601,608,686,707,709,713,720,724,738,763,774,877,887,891,1045,1066,1070,1081,1107,1118,1122,1137,1175,1188,1190,1194,1248,1250,1254,1257,1706,1712,1714,1718,1800,1807,1811,1854],[10,11,13],"h2",{"id":12},"the-preview-was-empty","The preview was empty",[15,16,17,18,22,23,26],"p",{},"This blog has a ",[19,20,21],"strong",{},"preview"," component: put HTML, CSS, and JS code in tabs inside a post, and it shows that code running. One day I noticed every preview box was ",[19,24,25],{},"empty",". Opening the code, there were two causes.",[28,29,31],"h3",{"id":30},"cause-1-the-iframe-path-was-never-implemented","Cause 1: the iframe path was never implemented",[15,33,34],{},"If there was a JS tab, it was supposed to render in an iframe, but that part was empty.",[36,37,42],"pre",{"className":38,"code":39,"language":40,"meta":41,"style":41},"language-js shiki shiki-themes github-light github-dark","const renderPreview = () => {\n  if (props.useIframe) {\n    generateCombinedContent();\n    \u002F\u002F iframe implementation could go here   ← builds an HTML string and puts it nowhere\n  } else {\n    renderWithShadowDOM();\n  }\n};\n","js","",[43,44,45,71,80,89,96,107,115,121],"code",{"__ignoreMap":41},[46,47,50,54,58,61,65,68],"span",{"class":48,"line":49},"line",1,[46,51,53],{"class":52},"szBVR","const",[46,55,57],{"class":56},"sScJk"," renderPreview",[46,59,60],{"class":52}," =",[46,62,64],{"class":63},"sVt8B"," () ",[46,66,67],{"class":52},"=>",[46,69,70],{"class":63}," {\n",[46,72,74,77],{"class":48,"line":73},2,[46,75,76],{"class":52},"  if",[46,78,79],{"class":63}," (props.useIframe) {\n",[46,81,83,86],{"class":48,"line":82},3,[46,84,85],{"class":56},"    generateCombinedContent",[46,87,88],{"class":63},"();\n",[46,90,92],{"class":48,"line":91},4,[46,93,95],{"class":94},"sJ8bj","    \u002F\u002F iframe implementation could go here   ← builds an HTML string and puts it nowhere\n",[46,97,99,102,105],{"class":48,"line":98},5,[46,100,101],{"class":63},"  } ",[46,103,104],{"class":52},"else",[46,106,70],{"class":63},[46,108,110,113],{"class":48,"line":109},6,[46,111,112],{"class":56},"    renderWithShadowDOM",[46,114,88],{"class":63},[46,116,118],{"class":48,"line":117},7,[46,119,120],{"class":63},"  }\n",[46,122,124],{"class":48,"line":123},8,[46,125,126],{"class":63},"};\n",[28,128,130],{"id":129},"cause-2-creating-a-shadow-dom-twice","Cause 2: creating a Shadow DOM twice",[15,132,133,134,137,138,141,142,145],{},"Without a JS tab, it rendered into a Shadow DOM. The problem was that the code arrived ",[19,135,136],{},"after"," the first render. The first render created a Shadow DOM, and when the code arrived and it re-rendered, it called ",[43,139,140],{},"attachShadow"," ",[19,143,144],{},"again",".",[36,147,149],{"className":38,"code":148,"language":40,"meta":41,"style":41},"const shadow = previewHost.value.attachShadow({ mode: \"open\" }); \u002F\u002F throws on the second call\n",[43,150,151],{"__ignoreMap":41},[46,152,153,155,159,161,164,166,169,173,176],{"class":48,"line":49},[46,154,53],{"class":52},[46,156,158],{"class":157},"sj4cs"," shadow",[46,160,60],{"class":52},[46,162,163],{"class":63}," previewHost.value.",[46,165,140],{"class":56},[46,167,168],{"class":63},"({ mode: ",[46,170,172],{"class":171},"sZZnC","\"open\"",[46,174,175],{"class":63}," }); ",[46,177,178],{"class":94},"\u002F\u002F throws on the second call\n",[15,180,181,183],{},[43,182,140],{}," can only be called once per element. Call it twice and Chromium throws:",[36,185,190],{"className":186,"code":188,"language":189},[187],"language-text","NotSupportedError: Failed to execute 'attachShadow' on 'Element':\nShadow root cannot be created on a host which already hosts a shadow tree.\n","text",[43,191,188],{"__ignoreMap":41},[15,193,194,195,198],{},"You can reuse an existing one through ",[43,196,197],{},"element.shadowRoot",". But while fixing it, a more fundamental problem showed up.",[200,201],"hr",{},[10,203,205],{"id":204},"why-an-iframe-instead-of-shadow-dom","Why an iframe instead of Shadow DOM",[207,208,209],"blockquote",{},[15,210,211,212,215],{},"A preview has to run post code ",[19,213,214],{},"like a standalone web page."," Shadow DOM only separates styles; it still lives in the same document.",[15,217,218],{},"I checked it in a browser:",[220,221,222,238],"table",{},[223,224,225],"thead",{},[226,227,228,232,235],"tr",{},[229,230,231],"th",{},"Check",[229,233,234],{},"Shadow DOM",[229,236,237],{},"iframe",[239,240,241,263,276],"tbody",{},[226,242,243,250,260],{},[244,245,246,247],"td",{},"Demo JS finds elements with ",[43,248,249],{},"querySelector",[244,251,252,255,256,259],{},[19,253,254],{},"No"," (",[43,257,258],{},"null",")",[244,261,262],{},"Yes",[226,264,265,268,274],{},[244,266,267],{},"Demo JS can affect the blog page",[244,269,270,271,259],{},"Yes (same ",[43,272,273],{},"window",[244,275,254],{},[226,277,278,288,291],{},[244,279,280,281,284,285],{},"Demo ",[43,282,283],{},"\u003Cdialog>",", ",[43,286,287],{},"alert()",[244,289,290],{},"Show over the blog",[244,292,293],{},"Show inside the preview",[15,295,296,297,300,301,145],{},"Elements inside a Shadow DOM aren't visible to the outer document's ",[43,298,299],{},"document.querySelector(\"#btn\")",". Demo JS runs in the outer document, so it gets ",[43,302,258],{},[15,304,305,306,309,310,312,313],{},"Most demo code starts with ",[43,307,308],{},"document.querySelector(...)",". Inside a Shadow DOM, that very first line returns ",[43,311,258],{}," and the demo breaks. So I ",[19,314,315],{},"switched every preview to a single iframe approach.",[200,317],{},[10,319,321],{"id":320},"srcdoc-a-document-inside-an-iframe-no-url-needed","srcdoc: a document inside an iframe, no URL needed",[15,323,324,325,328],{},"The simplest way to put HTML in an iframe is the ",[43,326,327],{},"srcdoc"," attribute. Pass an HTML string, no file or URL, and it becomes the iframe's document.",[36,330,332],{"className":38,"code":331,"language":40,"meta":41,"style":41},"const srcdoc = computed(() => {\n  const close = \"\u003C\u002F\" + \"script>\";\n  return `\u003C!doctype html>\n\u003Chtml lang=\"ko\">\n\u003Chead>\n\u003Cmeta charset=\"utf-8\">\n\u003Cstyle>body { margin: 0; padding: 1.5rem; display: flow-root; }\u003C\u002Fstyle>\n\u003Cstyle>${props.cssCode}\u003C\u002Fstyle>\n\u003C\u002Fhead>\n\u003Cbody>\n${props.htmlCode}\n\u003Cscript>${BOOTSTRAP}${close}\n${props.jsCode ? `\u003Cscript>${props.jsCode}${close}` : \"\"}\n\u003C\u002Fbody>\n\u003C\u002Fhtml>`;\n});\n",[43,333,334,353,375,383,388,393,398,403,419,425,431,447,464,501,507,515],{"__ignoreMap":41},[46,335,336,338,341,343,346,349,351],{"class":48,"line":49},[46,337,53],{"class":52},[46,339,340],{"class":157}," srcdoc",[46,342,60],{"class":52},[46,344,345],{"class":56}," computed",[46,347,348],{"class":63},"(() ",[46,350,67],{"class":52},[46,352,70],{"class":63},[46,354,355,358,361,363,366,369,372],{"class":48,"line":73},[46,356,357],{"class":52},"  const",[46,359,360],{"class":157}," close",[46,362,60],{"class":52},[46,364,365],{"class":171}," \"\u003C\u002F\"",[46,367,368],{"class":52}," +",[46,370,371],{"class":171}," \"script>\"",[46,373,374],{"class":63},";\n",[46,376,377,380],{"class":48,"line":82},[46,378,379],{"class":52},"  return",[46,381,382],{"class":171}," `\u003C!doctype html>\n",[46,384,385],{"class":48,"line":91},[46,386,387],{"class":171},"\u003Chtml lang=\"ko\">\n",[46,389,390],{"class":48,"line":98},[46,391,392],{"class":171},"\u003Chead>\n",[46,394,395],{"class":48,"line":109},[46,396,397],{"class":171},"\u003Cmeta charset=\"utf-8\">\n",[46,399,400],{"class":48,"line":117},[46,401,402],{"class":171},"\u003Cstyle>body { margin: 0; padding: 1.5rem; display: flow-root; }\u003C\u002Fstyle>\n",[46,404,405,408,411,413,416],{"class":48,"line":123},[46,406,407],{"class":171},"\u003Cstyle>${",[46,409,410],{"class":63},"props",[46,412,145],{"class":171},[46,414,415],{"class":63},"cssCode",[46,417,418],{"class":171},"}\u003C\u002Fstyle>\n",[46,420,422],{"class":48,"line":421},9,[46,423,424],{"class":171},"\u003C\u002Fhead>\n",[46,426,428],{"class":48,"line":427},10,[46,429,430],{"class":171},"\u003Cbody>\n",[46,432,434,437,439,441,444],{"class":48,"line":433},11,[46,435,436],{"class":171},"${",[46,438,410],{"class":63},[46,440,145],{"class":171},[46,442,443],{"class":63},"htmlCode",[46,445,446],{"class":171},"}\n",[46,448,450,453,456,459,462],{"class":48,"line":449},12,[46,451,452],{"class":171},"\u003Cscript>${",[46,454,455],{"class":157},"BOOTSTRAP",[46,457,458],{"class":171},"}${",[46,460,461],{"class":63},"close",[46,463,446],{"class":171},[46,465,467,469,471,473,476,479,482,484,486,488,490,492,495,498],{"class":48,"line":466},13,[46,468,436],{"class":171},[46,470,410],{"class":63},[46,472,145],{"class":171},[46,474,475],{"class":63},"jsCode",[46,477,478],{"class":52}," ?",[46,480,481],{"class":171}," `\u003Cscript>${",[46,483,410],{"class":63},[46,485,145],{"class":171},[46,487,475],{"class":63},[46,489,458],{"class":171},[46,491,461],{"class":63},[46,493,494],{"class":171},"}`",[46,496,497],{"class":52}," :",[46,499,500],{"class":171}," \"\"}\n",[46,502,504],{"class":48,"line":503},14,[46,505,506],{"class":171},"\u003C\u002Fbody>\n",[46,508,510,513],{"class":48,"line":509},15,[46,511,512],{"class":171},"\u003C\u002Fhtml>`",[46,514,374],{"class":63},[46,516,518],{"class":48,"line":517},16,[46,519,520],{"class":63},"});\n",[15,522,523,524,527,528,531,532,535,536,539],{},"The split ",[43,525,526],{},"\"\u003C\u002F\" + \"script>\""," is deliberate. Write ",[43,529,530],{},"\u003C\u002Fscript>"," literally inside a Vue file's ",[43,533,534],{},"\u003Cscript setup>",", and the Vue compiler reads it as ",[19,537,538],{},"the end of the component's script."," Splitting the string avoids that.",[36,541,545],{"className":542,"code":543,"language":544,"meta":41,"style":41},"language-html shiki shiki-themes github-light github-dark","\u003Ciframe\n  sandbox=\"allow-scripts allow-modals allow-forms\"\n  :srcdoc=\"srcdoc\"\n  :style=\"{ height: `${height}px` }\"\n>\u003C\u002Fiframe>\n","html",[43,546,547,556,567,577,587],{"__ignoreMap":41},[46,548,549,552],{"class":48,"line":49},[46,550,551],{"class":63},"\u003C",[46,553,555],{"class":554},"s9eBZ","iframe\n",[46,557,558,561,564],{"class":48,"line":73},[46,559,560],{"class":56},"  sandbox",[46,562,563],{"class":63},"=",[46,565,566],{"class":171},"\"allow-scripts allow-modals allow-forms\"\n",[46,568,569,572,574],{"class":48,"line":82},[46,570,571],{"class":56},"  :srcdoc",[46,573,563],{"class":63},[46,575,576],{"class":171},"\"srcdoc\"\n",[46,578,579,582,584],{"class":48,"line":91},[46,580,581],{"class":56},"  :style",[46,583,563],{"class":63},[46,585,586],{"class":171},"\"{ height: `${height}px` }\"\n",[46,588,589,592,594],{"class":48,"line":98},[46,590,591],{"class":63},">\u003C\u002F",[46,593,237],{"class":554},[46,595,596],{"class":63},">\n",[28,598,600],{"id":599},"sandbox-allow-only-what-you-need","sandbox: allow only what you need",[15,602,603,604,607],{},"With the ",[43,605,606],{},"sandbox"," attribute, almost everything inside the iframe is blocked except what you list.",[220,609,610,623],{},[223,611,612],{},[226,613,614,617,620],{},[229,615,616],{},"Value",[229,618,619],{},"Allows",[229,621,622],{},"Why it's here",[239,624,625,638,655,671],{},[226,626,627,632,635],{},[244,628,629],{},[43,630,631],{},"allow-scripts",[244,633,634],{},"Running scripts",[244,636,637],{},"JS demos and the height-reporting script",[226,639,640,645,652],{},[244,641,642],{},[43,643,644],{},"allow-modals",[244,646,647,284,649],{},[43,648,287],{},[43,650,651],{},"confirm()",[244,653,654],{},"Common in demos",[226,656,657,662,665],{},[244,658,659],{},[43,660,661],{},"allow-forms",[244,663,664],{},"Form submission",[244,666,667,668,670],{},"Form validation and ",[43,669,283],{}," close demos",[226,672,673,678,683],{},[244,674,675],{},[43,676,677],{},"allow-same-origin",[244,679,680],{},[19,681,682],{},"Not included",[244,684,685],{},"See below",[15,687,688,689,691,692,694,695,697,698,141,700,703,704,706],{},"I left out ",[43,690,677],{}," on purpose. A ",[43,693,327],{}," iframe is treated as the same origin as its parent page, and if you give it ",[43,696,631],{}," and ",[43,699,677],{},[19,701,702],{},"together",", scripts inside can reach the parent page and even remove their own ",[43,705,606],{}," attribute. Together, they make the sandbox almost pointless.",[200,708],{},[10,710,712],{"id":711},"fitting-the-iframe-height-to-its-content","Fitting the iframe height to its content",[15,714,715,716,719],{},"This was the real problem. An iframe ",[19,717,718],{},"doesn't grow when its content gets longer."," It clips at the default 150px, or if you pick a large height, you get empty space.",[28,721,723],{"id":722},"the-parent-cant-measure-inside-the-iframe","The parent can't measure inside the iframe",[15,725,726,727,730,731,733,734,737],{},"You'd think the parent could open the iframe's document (",[43,728,729],{},"contentDocument",") and read its height. But with a sandbox and no ",[43,732,677],{},", the inside counts as a ",[19,735,736],{},"different origin."," Here's what you actually get:",[36,739,741],{"className":38,"code":740,"language":40,"meta":41,"style":41},"document.getElementById(\"f\").contentDocument; \u002F\u002F null\n",[43,742,743],{"__ignoreMap":41},[46,744,745,748,751,754,757,760],{"class":48,"line":49},[46,746,747],{"class":63},"document.",[46,749,750],{"class":56},"getElementById",[46,752,753],{"class":63},"(",[46,755,756],{"class":171},"\"f\"",[46,758,759],{"class":63},").contentDocument; ",[46,761,762],{"class":94},"\u002F\u002F null\n",[15,764,765,766,769,770,773],{},"So I flipped it: ",[19,767,768],{},"the iframe measures itself and tells the parent",", using ",[43,771,772],{},"postMessage",", which sends messages between windows.",[36,775,777],{"className":38,"code":776,"language":40,"meta":41,"style":41},"\u002F\u002F script injected into the iframe (BOOTSTRAP)\nconst report = () =>\n  parent.postMessage({ codePreviewHeight: document.body.getBoundingClientRect().height }, \"*\");\n\nnew ResizeObserver(report).observe(document.body); \u002F\u002F whenever content changes size\naddEventListener(\"load\", report);                   \u002F\u002F after images etc. finish loading\naddEventListener(\"resize\", report);                 \u002F\u002F when iframe width changes and text rewraps\n",[43,778,779,784,798,820,826,846,862],{"__ignoreMap":41},[46,780,781],{"class":48,"line":49},[46,782,783],{"class":94},"\u002F\u002F script injected into the iframe (BOOTSTRAP)\n",[46,785,786,788,791,793,795],{"class":48,"line":73},[46,787,53],{"class":52},[46,789,790],{"class":56}," report",[46,792,60],{"class":52},[46,794,64],{"class":63},[46,796,797],{"class":52},"=>\n",[46,799,800,803,805,808,811,814,817],{"class":48,"line":82},[46,801,802],{"class":63},"  parent.",[46,804,772],{"class":56},[46,806,807],{"class":63},"({ codePreviewHeight: document.body.",[46,809,810],{"class":56},"getBoundingClientRect",[46,812,813],{"class":63},"().height }, ",[46,815,816],{"class":171},"\"*\"",[46,818,819],{"class":63},");\n",[46,821,822],{"class":48,"line":91},[46,823,825],{"emptyLinePlaceholder":824},true,"\n",[46,827,828,831,834,837,840,843],{"class":48,"line":98},[46,829,830],{"class":52},"new",[46,832,833],{"class":56}," ResizeObserver",[46,835,836],{"class":63},"(report).",[46,838,839],{"class":56},"observe",[46,841,842],{"class":63},"(document.body); ",[46,844,845],{"class":94},"\u002F\u002F whenever content changes size\n",[46,847,848,851,853,856,859],{"class":48,"line":109},[46,849,850],{"class":56},"addEventListener",[46,852,753],{"class":63},[46,854,855],{"class":171},"\"load\"",[46,857,858],{"class":63},", report);                   ",[46,860,861],{"class":94},"\u002F\u002F after images etc. finish loading\n",[46,863,864,866,868,871,874],{"class":48,"line":117},[46,865,850],{"class":56},[46,867,753],{"class":63},[46,869,870],{"class":171},"\"resize\"",[46,872,873],{"class":63},", report);                 ",[46,875,876],{"class":94},"\u002F\u002F when iframe width changes and text rewraps\n",[15,878,879,882,883,886],{},[43,880,881],{},"ResizeObserver"," is the key. When a demo button adds content or a ",[43,884,885],{},"\u003Cdetails>"," opens, it reports the new height.",[28,888,890],{"id":889},"the-parent-checks-the-message-came-from-its-own-iframe","The parent checks the message came from its own iframe",[36,892,894],{"className":38,"code":893,"language":40,"meta":41,"style":41},"const onMessage = (e) => {\n  if (e.source !== frame.value?.contentWindow) return; \u002F\u002F ignore other iframes and ads\n  const h = e.data?.codePreviewHeight;\n  if (typeof h === \"number\") height.value = Math.max(80, Math.ceil(h));\n};\n\nonMounted(() => window.addEventListener(\"message\", onMessage));\nonBeforeUnmount(() => window.removeEventListener(\"message\", onMessage));\n",[43,895,896,918,940,952,995,999,1003,1025],{"__ignoreMap":41},[46,897,898,900,903,905,907,911,914,916],{"class":48,"line":49},[46,899,53],{"class":52},[46,901,902],{"class":56}," onMessage",[46,904,60],{"class":52},[46,906,255],{"class":63},[46,908,910],{"class":909},"s4XuR","e",[46,912,913],{"class":63},") ",[46,915,67],{"class":52},[46,917,70],{"class":63},[46,919,920,922,925,928,931,934,937],{"class":48,"line":73},[46,921,76],{"class":52},[46,923,924],{"class":63}," (e.source ",[46,926,927],{"class":52},"!==",[46,929,930],{"class":63}," frame.value?.contentWindow) ",[46,932,933],{"class":52},"return",[46,935,936],{"class":63},"; ",[46,938,939],{"class":94},"\u002F\u002F ignore other iframes and ads\n",[46,941,942,944,947,949],{"class":48,"line":82},[46,943,357],{"class":52},[46,945,946],{"class":157}," h",[46,948,60],{"class":52},[46,950,951],{"class":63}," e.data?.codePreviewHeight;\n",[46,953,954,956,958,961,964,967,970,973,975,978,981,983,986,989,992],{"class":48,"line":91},[46,955,76],{"class":52},[46,957,255],{"class":63},[46,959,960],{"class":52},"typeof",[46,962,963],{"class":63}," h ",[46,965,966],{"class":52},"===",[46,968,969],{"class":171}," \"number\"",[46,971,972],{"class":63},") height.value ",[46,974,563],{"class":52},[46,976,977],{"class":63}," Math.",[46,979,980],{"class":56},"max",[46,982,753],{"class":63},[46,984,985],{"class":157},"80",[46,987,988],{"class":63},", Math.",[46,990,991],{"class":56},"ceil",[46,993,994],{"class":63},"(h));\n",[46,996,997],{"class":48,"line":98},[46,998,126],{"class":63},[46,1000,1001],{"class":48,"line":109},[46,1002,825],{"emptyLinePlaceholder":824},[46,1004,1005,1008,1010,1012,1015,1017,1019,1022],{"class":48,"line":117},[46,1006,1007],{"class":56},"onMounted",[46,1009,348],{"class":63},[46,1011,67],{"class":52},[46,1013,1014],{"class":63}," window.",[46,1016,850],{"class":56},[46,1018,753],{"class":63},[46,1020,1021],{"class":171},"\"message\"",[46,1023,1024],{"class":63},", onMessage));\n",[46,1026,1027,1030,1032,1034,1036,1039,1041,1043],{"class":48,"line":123},[46,1028,1029],{"class":56},"onBeforeUnmount",[46,1031,348],{"class":63},[46,1033,67],{"class":52},[46,1035,1014],{"class":63},[46,1037,1038],{"class":56},"removeEventListener",[46,1040,753],{"class":63},[46,1042,1021],{"class":171},[46,1044,1024],{"class":63},[15,1046,1047,1050,1051,1054,1055,1058,1059,1062,1063,145],{},[43,1048,1049],{},"message"," events arrive from ",[19,1052,1053],{},"every"," iframe on the page. A post can have several previews plus ad iframes, so without checking the sending window via ",[43,1056,1057],{},"e.source",", one preview can pick up another's height. Checking the origin (",[43,1060,1061],{},"e.origin",") doesn't work here, because a sandboxed iframe's origin is ",[43,1064,1065],{},"\"null\"",[28,1067,1069],{"id":1068},"trap-1-scrollheight-never-shrinks","Trap 1: scrollHeight never shrinks",[15,1071,1072,1073,1076,1077,1080],{},"It's tempting to measure ",[43,1074,1075],{},"document.documentElement.scrollHeight"," inside the iframe, but that value ",[19,1078,1079],{},"never gets smaller than the iframe's current height."," Put one line of content in a 400px-tall iframe and measure:",[36,1082,1084],{"className":38,"code":1083,"language":40,"meta":41,"style":41},"document.documentElement.scrollHeight;        \u002F\u002F 400 (the iframe's height)\ndocument.body.getBoundingClientRect().height; \u002F\u002F the real content height\n",[43,1085,1086,1094],{"__ignoreMap":41},[46,1087,1088,1091],{"class":48,"line":49},[46,1089,1090],{"class":63},"document.documentElement.scrollHeight;        ",[46,1092,1093],{"class":94},"\u002F\u002F 400 (the iframe's height)\n",[46,1095,1096,1099,1101,1104],{"class":48,"line":73},[46,1097,1098],{"class":63},"document.body.",[46,1100,810],{"class":56},[46,1102,1103],{"class":63},"().height; ",[46,1105,1106],{"class":94},"\u002F\u002F the real content height\n",[15,1108,1109,1110,1113,1114,1117],{},"Measure with ",[43,1111,1112],{},"scrollHeight"," and the iframe can grow but ",[19,1115,1116],{},"never shrink back."," So I measured the body's real size.",[28,1119,1121],{"id":1120},"trap-2-margins-escape-the-body","Trap 2: margins escape the body",[15,1123,1124,1125,1128,1129,1132,1133,1136],{},"Even the body measurement can come out oddly small. The first child's top margin and the last child's bottom margin ",[19,1126,1127],{},"escape outside the body"," (margin collapsing). With ",[43,1130,1131],{},"body { margin: 0 }"," and one ",[43,1134,1135],{},"\u003Cp>"," with 40px top and bottom margins, the difference is clear:",[220,1138,1139,1149],{},[223,1140,1141],{},[226,1142,1143,1146],{},[229,1144,1145],{},"Body style",[229,1147,1148],{},"Measured height",[239,1150,1151,1162],{},[226,1152,1153,1156],{},[244,1154,1155],{},"Default",[244,1157,1158,1161],{},[19,1159,1160],{},"18px",". 80px of margin is lost and content gets cut off",[226,1163,1164,1169],{},[244,1165,1166],{},[43,1167,1168],{},"display: flow-root",[244,1170,1171,1174],{},[19,1172,1173],{},"98px",". Margins included",[15,1176,1177,1179,1180,1183,1184,1187],{},[43,1178,1168],{}," makes the body its own block formatting context, so children's margins don't leak out. People used tricks like ",[43,1181,1182],{},"overflow: hidden"," or ",[43,1185,1186],{},"padding-top: 1px"," for this; now one line does it.",[200,1189],{},[10,1191,1193],{"id":1192},"other-things-i-handled","Other things I handled",[220,1195,1196,1206],{},[223,1197,1198],{},[226,1199,1200,1203],{},[229,1201,1202],{},"Problem",[229,1204,1205],{},"Handling",[239,1207,1208,1229],{},[226,1209,1210,1213],{},[244,1211,1212],{},"Submitting a form in a demo navigates the iframe to a blank page",[244,1214,1215,1218,1219,1222,1223,1226,1227],{},[43,1216,1217],{},"preventDefault()"," on ",[43,1220,1221],{},"submit",", except ",[43,1224,1225],{},"method=\"dialog\""," forms that close a ",[43,1228,283],{},[226,1230,1231,1234],{},[244,1232,1233],{},"The blog's CSP (Content-Security-Policy) blocks demos",[244,1235,1236,1237,1239,1240,1243,1244,1247],{},"A ",[43,1238,327],{}," iframe ",[19,1241,1242],{},"inherits the parent page's CSP."," This blog keeps ",[43,1245,1246],{},"'unsafe-inline'"," because of the demos' inline scripts",[200,1249],{},[10,1251,1253],{"id":1252},"try-it","Try it",[15,1255,1256],{},"The preview below is the component from this post. Click \"Add item\" and the preview grows with its content.",[1258,1259,1260,1359,1543],"code-group",{},[36,1261,1263],{"className":542,"code":1262,"language":544,"meta":41,"style":41},"\u003Cbutton id=\"add\">Add item\u003C\u002Fbutton>\n\u003Cbutton id=\"reset\">Reset\u003C\u002Fbutton>\n\u003Cul id=\"list\">\u003Cli>Item 1\u003C\u002Fli>\u003C\u002Ful>\n\u003Cp class=\"help\">The preview height follows the content.\u003C\u002Fp>\n",[43,1264,1265,1287,1307,1338],{"__ignoreMap":41},[46,1266,1267,1269,1272,1275,1277,1280,1283,1285],{"class":48,"line":49},[46,1268,551],{"class":63},[46,1270,1271],{"class":554},"button",[46,1273,1274],{"class":56}," id",[46,1276,563],{"class":63},[46,1278,1279],{"class":171},"\"add\"",[46,1281,1282],{"class":63},">Add item\u003C\u002F",[46,1284,1271],{"class":554},[46,1286,596],{"class":63},[46,1288,1289,1291,1293,1295,1297,1300,1303,1305],{"class":48,"line":73},[46,1290,551],{"class":63},[46,1292,1271],{"class":554},[46,1294,1274],{"class":56},[46,1296,563],{"class":63},[46,1298,1299],{"class":171},"\"reset\"",[46,1301,1302],{"class":63},">Reset\u003C\u002F",[46,1304,1271],{"class":554},[46,1306,596],{"class":63},[46,1308,1309,1311,1314,1316,1318,1321,1324,1327,1330,1332,1334,1336],{"class":48,"line":82},[46,1310,551],{"class":63},[46,1312,1313],{"class":554},"ul",[46,1315,1274],{"class":56},[46,1317,563],{"class":63},[46,1319,1320],{"class":171},"\"list\"",[46,1322,1323],{"class":63},">\u003C",[46,1325,1326],{"class":554},"li",[46,1328,1329],{"class":63},">Item 1\u003C\u002F",[46,1331,1326],{"class":554},[46,1333,591],{"class":63},[46,1335,1313],{"class":554},[46,1337,596],{"class":63},[46,1339,1340,1342,1344,1347,1349,1352,1355,1357],{"class":48,"line":91},[46,1341,551],{"class":63},[46,1343,15],{"class":554},[46,1345,1346],{"class":56}," class",[46,1348,563],{"class":63},[46,1350,1351],{"class":171},"\"help\"",[46,1353,1354],{"class":63},">The preview height follows the content.\u003C\u002F",[46,1356,15],{"class":554},[46,1358,596],{"class":63},[36,1360,1364],{"className":1361,"code":1362,"language":1363,"meta":41,"style":41},"language-css shiki shiki-themes github-light github-dark","button { font: inherit; padding: 6px 12px; border: 1px solid #ccc; border-radius: 8px; background: #fff; cursor: pointer; }\nul { margin: 16px 0; padding-left: 20px; }\nli { padding: 4px 0; }\n.help { margin: 0; color: #666; font-size: 14px; }\n","css",[43,1365,1366,1453,1486,1505],{"__ignoreMap":41},[46,1367,1368,1370,1373,1376,1379,1382,1384,1387,1389,1392,1395,1398,1400,1402,1405,1407,1410,1412,1415,1418,1420,1423,1425,1428,1430,1432,1435,1437,1440,1442,1445,1447,1450],{"class":48,"line":49},[46,1369,1271],{"class":554},[46,1371,1372],{"class":63}," { ",[46,1374,1375],{"class":157},"font",[46,1377,1378],{"class":63},": ",[46,1380,1381],{"class":157},"inherit",[46,1383,936],{"class":63},[46,1385,1386],{"class":157},"padding",[46,1388,1378],{"class":63},[46,1390,1391],{"class":157},"6",[46,1393,1394],{"class":52},"px",[46,1396,1397],{"class":157}," 12",[46,1399,1394],{"class":52},[46,1401,936],{"class":63},[46,1403,1404],{"class":157},"border",[46,1406,1378],{"class":63},[46,1408,1409],{"class":157},"1",[46,1411,1394],{"class":52},[46,1413,1414],{"class":157}," solid",[46,1416,1417],{"class":157}," #ccc",[46,1419,936],{"class":63},[46,1421,1422],{"class":157},"border-radius",[46,1424,1378],{"class":63},[46,1426,1427],{"class":157},"8",[46,1429,1394],{"class":52},[46,1431,936],{"class":63},[46,1433,1434],{"class":157},"background",[46,1436,1378],{"class":63},[46,1438,1439],{"class":157},"#fff",[46,1441,936],{"class":63},[46,1443,1444],{"class":157},"cursor",[46,1446,1378],{"class":63},[46,1448,1449],{"class":157},"pointer",[46,1451,1452],{"class":63},"; }\n",[46,1454,1455,1457,1459,1462,1464,1467,1469,1472,1474,1477,1479,1482,1484],{"class":48,"line":73},[46,1456,1313],{"class":554},[46,1458,1372],{"class":63},[46,1460,1461],{"class":157},"margin",[46,1463,1378],{"class":63},[46,1465,1466],{"class":157},"16",[46,1468,1394],{"class":52},[46,1470,1471],{"class":157}," 0",[46,1473,936],{"class":63},[46,1475,1476],{"class":157},"padding-left",[46,1478,1378],{"class":63},[46,1480,1481],{"class":157},"20",[46,1483,1394],{"class":52},[46,1485,1452],{"class":63},[46,1487,1488,1490,1492,1494,1496,1499,1501,1503],{"class":48,"line":82},[46,1489,1326],{"class":554},[46,1491,1372],{"class":63},[46,1493,1386],{"class":157},[46,1495,1378],{"class":63},[46,1497,1498],{"class":157},"4",[46,1500,1394],{"class":52},[46,1502,1471],{"class":157},[46,1504,1452],{"class":63},[46,1506,1507,1510,1512,1514,1516,1519,1521,1524,1526,1529,1531,1534,1536,1539,1541],{"class":48,"line":91},[46,1508,1509],{"class":56},".help",[46,1511,1372],{"class":63},[46,1513,1461],{"class":157},[46,1515,1378],{"class":63},[46,1517,1518],{"class":157},"0",[46,1520,936],{"class":63},[46,1522,1523],{"class":157},"color",[46,1525,1378],{"class":63},[46,1527,1528],{"class":157},"#666",[46,1530,936],{"class":63},[46,1532,1533],{"class":157},"font-size",[46,1535,1378],{"class":63},[46,1537,1538],{"class":157},"14",[46,1540,1394],{"class":52},[46,1542,1452],{"class":63},[36,1544,1546],{"className":38,"code":1545,"language":40,"meta":41,"style":41},"const list = document.querySelector(\"#list\");\ndocument.querySelector(\"#add\").addEventListener(\"click\", () => {\n  const li = document.createElement(\"li\");\n  li.textContent = `Item ${list.children.length + 1}`;\n  list.append(li);\n});\ndocument.querySelector(\"#reset\").addEventListener(\"click\", () => {\n  list.innerHTML = \"\u003Cli>Item 1\u003C\u002Fli>\";\n});\n",[43,1547,1548,1569,1597,1618,1650,1661,1665,1690,1702],{"__ignoreMap":41},[46,1549,1550,1552,1555,1557,1560,1562,1564,1567],{"class":48,"line":49},[46,1551,53],{"class":52},[46,1553,1554],{"class":157}," list",[46,1556,60],{"class":52},[46,1558,1559],{"class":63}," document.",[46,1561,249],{"class":56},[46,1563,753],{"class":63},[46,1565,1566],{"class":171},"\"#list\"",[46,1568,819],{"class":63},[46,1570,1571,1573,1575,1577,1580,1583,1585,1587,1590,1593,1595],{"class":48,"line":73},[46,1572,747],{"class":63},[46,1574,249],{"class":56},[46,1576,753],{"class":63},[46,1578,1579],{"class":171},"\"#add\"",[46,1581,1582],{"class":63},").",[46,1584,850],{"class":56},[46,1586,753],{"class":63},[46,1588,1589],{"class":171},"\"click\"",[46,1591,1592],{"class":63},", () ",[46,1594,67],{"class":52},[46,1596,70],{"class":63},[46,1598,1599,1601,1604,1606,1608,1611,1613,1616],{"class":48,"line":82},[46,1600,357],{"class":52},[46,1602,1603],{"class":157}," li",[46,1605,60],{"class":52},[46,1607,1559],{"class":63},[46,1609,1610],{"class":56},"createElement",[46,1612,753],{"class":63},[46,1614,1615],{"class":171},"\"li\"",[46,1617,819],{"class":63},[46,1619,1620,1623,1625,1628,1631,1633,1636,1638,1641,1643,1646,1648],{"class":48,"line":91},[46,1621,1622],{"class":63},"  li.textContent ",[46,1624,563],{"class":52},[46,1626,1627],{"class":171}," `Item ${",[46,1629,1630],{"class":63},"list",[46,1632,145],{"class":171},[46,1634,1635],{"class":63},"children",[46,1637,145],{"class":171},[46,1639,1640],{"class":157},"length",[46,1642,368],{"class":52},[46,1644,1645],{"class":157}," 1",[46,1647,494],{"class":171},[46,1649,374],{"class":63},[46,1651,1652,1655,1658],{"class":48,"line":98},[46,1653,1654],{"class":63},"  list.",[46,1656,1657],{"class":56},"append",[46,1659,1660],{"class":63},"(li);\n",[46,1662,1663],{"class":48,"line":109},[46,1664,520],{"class":63},[46,1666,1667,1669,1671,1673,1676,1678,1680,1682,1684,1686,1688],{"class":48,"line":117},[46,1668,747],{"class":63},[46,1670,249],{"class":56},[46,1672,753],{"class":63},[46,1674,1675],{"class":171},"\"#reset\"",[46,1677,1582],{"class":63},[46,1679,850],{"class":56},[46,1681,753],{"class":63},[46,1683,1589],{"class":171},[46,1685,1592],{"class":63},[46,1687,67],{"class":52},[46,1689,70],{"class":63},[46,1691,1692,1695,1697,1700],{"class":48,"line":123},[46,1693,1694],{"class":63},"  list.innerHTML ",[46,1696,563],{"class":52},[46,1698,1699],{"class":171}," \"\u003Cli>Item 1\u003C\u002Fli>\"",[46,1701,374],{"class":63},[46,1703,1704],{"class":48,"line":421},[46,1705,520],{"class":63},[15,1707,1708,1709,1711],{},"Click \"Reset\" and the height shrinks again. With ",[43,1710,1112],{},", it wouldn't.",[200,1713],{},[10,1715,1717],{"id":1716},"summary","Summary",[220,1719,1720,1730],{},[223,1721,1722],{},[226,1723,1724,1727],{},[229,1725,1726],{},"Goal",[229,1728,1729],{},"How",[239,1731,1732,1745,1758,1768,1780,1790],{},[226,1733,1734,1737],{},[244,1735,1736],{},"Run code like a standalone page",[244,1738,1739,1741,1742,1744],{},[43,1740,237],{}," + ",[43,1743,327],{},", not Shadow DOM",[226,1746,1747,1750],{},[244,1748,1749],{},"Run it safely",[244,1751,1752,1753,1755,1756],{},"Add ",[43,1754,606],{},", without ",[43,1757,677],{},[226,1759,1760,1763],{},[244,1761,1762],{},"Measure height",[244,1764,1765,1766,259],{},"Measure the body inside the iframe (",[43,1767,810],{},[226,1769,1770,1773],{},[244,1771,1772],{},"Report height",[244,1774,1775,1741,1777],{},[43,1776,881],{},[43,1778,1779],{},"parent.postMessage",[226,1781,1782,1785],{},[244,1783,1784],{},"Receive messages",[244,1786,1787,1788],{},"Check the sender with ",[43,1789,1057],{},[226,1791,1792,1795],{},[244,1793,1794],{},"Escaping margins",[244,1796,1797,1799],{},[43,1798,1168],{}," on the body",[15,1801,1802,1803,1806],{},"It's easy to assume \"the parent can just measure it,\" but the moment you add a sandbox, the parent can't see inside. Switch to ",[19,1804,1805],{},"measuring inside and reporting by message",", and you get both the sandbox and auto height.",[10,1808,1810],{"id":1809},"references","References",[1313,1812,1813,1826,1833,1840,1847],{},[1326,1814,1815],{},[1816,1817,1821,1822,1825],"a",{"href":1818,"rel":1819},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FHTML\u002FElement\u002Fiframe",[1820],"nofollow","MDN: ",[43,1823,1824],{},"\u003Ciframe>"," (sandbox, srcdoc)",[1326,1827,1828],{},[1816,1829,1832],{"href":1830,"rel":1831},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FWindow\u002FpostMessage",[1820],"MDN: Window.postMessage()",[1326,1834,1835],{},[1816,1836,1839],{"href":1837,"rel":1838},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FResizeObserver",[1820],"MDN: ResizeObserver",[1326,1841,1842],{},[1816,1843,1846],{"href":1844,"rel":1845},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FElement\u002FattachShadow",[1820],"MDN: Element.attachShadow()",[1326,1848,1849],{},[1816,1850,1853],{"href":1851,"rel":1852},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FCSS\u002FCSS_display\u002FBlock_formatting_context",[1820],"MDN: Block formatting context (display: flow-root)",[1855,1856,1857],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":41,"searchDepth":73,"depth":73,"links":1859},[1860,1864,1865,1868,1874,1875,1876,1877],{"id":12,"depth":73,"text":13,"children":1861},[1862,1863],{"id":30,"depth":82,"text":31},{"id":129,"depth":82,"text":130},{"id":204,"depth":73,"text":205},{"id":320,"depth":73,"text":321,"children":1866},[1867],{"id":599,"depth":82,"text":600},{"id":711,"depth":73,"text":712,"children":1869},[1870,1871,1872,1873],{"id":722,"depth":82,"text":723},{"id":889,"depth":82,"text":890},{"id":1068,"depth":82,"text":1069},{"id":1120,"depth":82,"text":1121},{"id":1192,"depth":73,"text":1193},{"id":1252,"depth":73,"text":1253},{"id":1716,"depth":73,"text":1717},{"id":1809,"depth":73,"text":1810},"Fixing a blog's empty code preview: why I chose a sandboxed iframe over Shadow DOM, and how to fit iframe height to its content (postMessage, ResizeObserver, and the margin trap).","md",{"date":1881,"field":1882,"tags":1883},"2026.10.16","frontend",[544,1884],"javascript","\u002Fen\u002Fiframe-auto-height-code-preview",{"title":5,"description":1878},{"loc":1885},"en\u002Fiframe-auto-height-code-preview","nJ5Hy_ngCiDu1eXa4trAn9UnayEzmcUpVTkmLA4ouNw",1792136461624]