{"id":11329,"date":"2026-07-17T21:51:43","date_gmt":"2026-07-17T16:21:43","guid":{"rendered":"https:\/\/4ksamachar.com\/?p=11329"},"modified":"2026-07-17T21:51:43","modified_gmt":"2026-07-17T16:21:43","slug":"gutenberg-times-the-post-editor-is-going-full-iframe-what-block-developers-need-to-know-before-wordpress-7-1","status":"publish","type":"post","link":"https:\/\/4ksamachar.com\/?p=11329","title":{"rendered":"Gutenberg Times: The post editor is going full iframe: what block developers need to know before WordPress 7.1"},"content":{"rendered":"<p class=\"wp-block-paragraph\">For years, the post editor has lived a double life. The Site Editor renders your blocks inside an iframe. The post editor \u2014 where most people actually spend their time \u2014 renders them directly in the admin page. That split ends with WordPress 7.1: <strong>the post editor canvas will always be an iframe, on every theme, no matter what <code>apiVersion<\/code> your blocks declare<\/strong>. The Gutenberg plugin has been enforcing exactly this for months. If you ship blocks, assume the iframe.<\/p>\n<p class=\"wp-block-paragraph\">If your block never touches the global <code>document<\/code> or <code>window<\/code>, you can probably stop reading after you\u2019ve changed <code>\"apiVersion\": 2<\/code> to <code>\"apiVersion\": 3<\/code> in <code>block.json<\/code>. For everyone else \u2014 and especially anyone shipping blocks that wrap third-party libraries \u2014 the iframe changes <em>where your code runs<\/em> versus <em>where your markup lives<\/em>. That gap is where things break.<\/p>\n<p class=\"wp-block-paragraph\"><strong>Quick reference guide<\/strong>: <strong>Are your blocks ready?<\/strong><\/p>\n<figure class=\"wp-block-image size-large\"><img fetchpriority=\"high\" decoding=\"async\" alt=\"An infographic showing the checks and fixes for readying custom blocks for the WordPress 7.1 iframed editor\" class=\"wp-image-46098\" height=\"652\" src=\"https:\/\/i0.wp.com\/gutenbergtimes.com\/wp-content\/uploads\/2026\/07\/7.1-iframe-block-check.png?resize=652%2C652&amp;ssl=1\" width=\"652\"><\/figure>\n<h2 class=\"wp-block-heading\">The timeline, in one table<\/h2>\n<figure class=\"wp-block-table\">\n<table class=\"has-fixed-layout\">\n<thead>\n<tr>\n<th>Release<\/th>\n<th>What happens<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><strong>June 21, 2021<\/strong><\/td>\n<td>The iframed editor was <a href=\"https:\/\/make.wordpress.org\/core\/2021\/06\/29\/blocks-in-an-iframed-template-editor\/\" target=\"_blank\" rel=\"noopener\">announced on make.wordpress.org<\/a><\/td>\n<\/tr>\n<tr>\n<td><strong>WordPress 6.9<\/strong> (Dec 2025)<\/td>\n<td>Console warning (with <code>SCRIPT_DEBUG<\/code>) when a block registers with <code>apiVersion<\/code> 2 or lower. The <code>block.json<\/code> schema now only validates <code>apiVersion: 3<\/code>.<\/td>\n<\/tr>\n<tr>\n<td><strong>WordPress 7.0<\/strong> (Apr 2026)<\/td>\n<td>The iframe decision now looks at blocks <strong>actually inserted in the post<\/strong>, not every registered block. All inserted blocks on v3+ \u2192 canvas is iframed. Insert a single v1\/v2 block \u2192 the iframe is removed on the fly. Nothing is enforced yet.<\/td>\n<\/tr>\n<tr>\n<td><strong>Gutenberg 22.6+<\/strong><\/td>\n<td>The iframe is <strong>enforced regardless of theme<\/strong> \u2014 this is the feedback-gathering phase.<\/td>\n<\/tr>\n<tr>\n<td><strong>WordPress 7.1<\/strong> (Aug 19, 2026)<\/td>\n<td>The iframe is <strong>enforced on every theme<\/strong>, regardless of <code>apiVersion<\/code>. The conditions are gone, not tightened.<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p class=\"wp-block-paragraph\">The WordPress 7.0 change is subtle but important: before 7.0, one <code>apiVersion: 2<\/code> block <em>registered by any active plugin<\/em> \u2014 even one never used in the post \u2014 kept the entire editor out of the iframe for everyone. Now only inserted blocks count. Your v3 block gets the iframe until the user inserts a legacy one, at which point the editor quietly reloads the canvas without the iframe. The companion plugin ships a <code>legacy-api-v2<\/code> block so you can watch this happen \u2014 insert it into an otherwise-v3 post and the iframe disappears. In 7.1, that escape hatch closes.<\/p>\n<p class=\"wp-block-paragraph\">Worth knowing, as an aside: the \u201cevery theme\u201d decision landed in <a href=\"https:\/\/wordpress.org\/news\/2026\/07\/wordpress-7-1-beta-1\/\" rel=\"noopener\" target=\"_blank\">WordPress 7.1 Beta 1<\/a>, and it\u2019s deliberately being tested in public. Gutenberg merged <a href=\"https:\/\/github.com\/WordPress\/gutenberg\/pull\/74042\" rel=\"noopener\" target=\"_blank\">\u201cPost editor: always iframe\u201d (#74042)<\/a> on July 10, 2026, deleting the theme and <code>apiVersion<\/code> conditions outright. The 7.1 release lead signed off on that merge <a href=\"https:\/\/github.com\/WordPress\/gutenberg\/pull\/74042#issuecomment-4878384706\" rel=\"noopener\" target=\"_blank\">on the condition<\/a> that the team could \u201cmove to the softer approach\u201d if Beta 1 feedback surfaced real problems \u2014 the softer approach being enforcement on block themes only, with everything else staying on the 7.0 rules. No specific mechanism is committed to; the plan is to respond to what the beta actually turns up.<br \/>Which is a reason to test harder, not to wait and see. If that rollback happens, the iframed and non-iframed editors both stay in the wild <em>longer<\/em> \u2014 and your block has to work in both regardless of which way it goes.<\/p>\n<p class=\"wp-block-paragraph\">It\u2019s also worth noting that blocks that will break with the 7.1 changes are most likely <strong>already breaking in the Site Editor<\/strong>.<\/p>\n<h2 class=\"wp-block-heading\">Why the iframe is a good thing<\/h2>\n<p class=\"wp-block-paragraph\">This isn\u2019t change for change sake. Rendering the canvas in an iframe gives the editor a real document boundary:<\/p>\n<ul class=\"wp-block-list\">\n<li><strong>Admin CSS stops leaking into your content.<\/strong> No more <code>#wpadminbar<\/code>-adjacent style resets, no more admin styles subtly changing how blocks render in the editor versus the front end.<\/li>\n<li><strong>Viewport units and media queries finally work.<\/strong> <code>vw<\/code>, <code>vh<\/code>, and <code>@media<\/code> rules resolve against the canvas, not the admin page \u2014 so tablet\/mobile previews and zoomed-out views actually behave like the front end.<\/li>\n<li><strong>What you see is much closer to what you get.<\/strong> The canvas document is built from your theme\u2019s styles, not the admin\u2019s.<\/li>\n<\/ul>\n<p class=\"wp-block-paragraph\">The issue this raises for block developers? Your editor JavaScript runs in the admin page, but your block\u2019s DOM lives in a different document. Every assumption baked into <code>document.querySelector(...)<\/code> and <code>window.addEventListener(...)<\/code> just became wrong.<\/p>\n<h2 class=\"wp-block-heading\">What actually breaks (and how to fix it)<\/h2>\n<p class=\"wp-block-paragraph\">Everything below is demonstrable with the companion plugin \u2014 each pattern ships as a broken\/fixed pair of blocks: <strong><a href=\"https:\/\/github.com\/ryanwelcher\/iframe-editor-examples\" rel=\"noopener\" target=\"_blank\">iframe-editor-examples on GitHub<\/a><\/strong>.<\/p>\n<h3 class=\"wp-block-heading\">1. Global <code>window<\/code> and <code>document<\/code> references<\/h3>\n<p class=\"wp-block-paragraph\">The classic: a block that reads the viewport or listens for resize.<\/p>\n<div class=\"wp-block-kevinbatdorf-code-block-pro\"><span><span>JavaScript<\/span><\/span><span class=\"code-block-pro-copy-button\"><\/p>\n<pre class=\"code-block-pro-copy-button-pre\"><textarea class=\"code-block-pro-copy-button-textarea\" readonly>\/\/ <img decoding=\"async\" alt=\"\u274c\" class=\"wp-smiley\" src=\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/274c.png\"> Broken in the iframed editor\nuseEffect( () =&gt; {\n\tconst update = () =&gt; setWidth( window.innerWidth );\n\tupdate();\n\twindow.addEventListener( 'resize', update );\n\treturn () =&gt; window.removeEventListener( 'resize', update );\n}, [] );<\/textarea><\/pre>\n<p><\/span><\/p>\n<pre class=\"shiki dark-plus\"><code><span class=\"line\"><span>\/\/ \u274c Broken in the iframed editor<\/span><\/span>\n<span class=\"line\"><span>useEffect<\/span><span>( () <\/span><span>=&gt;<\/span><span> {<\/span><\/span>\n<span class=\"line\"><span>\t<\/span><span>const<\/span><span> <\/span><span>update<\/span><span> = () <\/span><span>=&gt;<\/span><span> <\/span><span>setWidth<\/span><span>( <\/span><span>window<\/span><span>.<\/span><span>innerWidth<\/span><span> );<\/span><\/span>\n<span class=\"line\"><span>\t<\/span><span>update<\/span><span>();<\/span><\/span>\n<span class=\"line\"><span>\t<\/span><span>window<\/span><span>.<\/span><span>addEventListener<\/span><span>( <\/span><span>'resize'<\/span><span>, <\/span><span>update<\/span><span> );<\/span><\/span>\n<span class=\"line\"><span>\t<\/span><span>return<\/span><span> () <\/span><span>=&gt;<\/span><span> <\/span><span>window<\/span><span>.<\/span><span>removeEventListener<\/span><span>( <\/span><span>'resize'<\/span><span>, <\/span><span>update<\/span><span> );<\/span><\/span>\n<span class=\"line\"><span>}, [] );<\/span><\/span><\/code><\/pre>\n<\/div>\n<p class=\"wp-block-paragraph\">Editor scripts load in the admin page, so <code>window<\/code> is the <strong>admin<\/strong> window. In the iframed editor this reports the wrong width and never reacts to the canvas resizing \u2014 switch to the Tablet preview and the number doesn\u2019t move.<\/p>\n<p class=\"wp-block-paragraph\">The fix is to derive the document and window from your block\u2019s own DOM element:<\/p>\n<div class=\"wp-block-kevinbatdorf-code-block-pro\"><span><span>JavaScript<\/span><\/span><span class=\"code-block-pro-copy-button\"><\/p>\n<pre class=\"code-block-pro-copy-button-pre\"><textarea class=\"code-block-pro-copy-button-textarea\" readonly>\/\/ <img decoding=\"async\" alt=\"\u2705\" class=\"wp-smiley\" src=\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/2705.png\"> Fixed \u2014 works iframed or not\nimport { useRefEffect } from '@wordpress\/compose';\n\nconst ref = useRefEffect( ( element ) =&gt; {\n\tconst { defaultView } = element.ownerDocument;\n\tconst update = () =&gt; setWidth( defaultView.innerWidth );\n\tupdate();\n\tdefaultView.addEventListener( 'resize', update );\n\treturn () =&gt; defaultView.removeEventListener( 'resize', update );\n}, [] );\n\nconst blockProps = useBlockProps( { ref } );<\/textarea><\/pre>\n<p><\/span><\/p>\n<pre class=\"shiki dark-plus\"><code><span class=\"line\"><span>\/\/ \u2705 Fixed \u2014 works iframed or not<\/span><\/span>\n<span class=\"line\"><span>import<\/span><span> { <\/span><span>useRefEffect<\/span><span> } <\/span><span>from<\/span><span> <\/span><span>'@wordpress\/compose'<\/span><span>;<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span>const<\/span><span> <\/span><span>ref<\/span><span> = <\/span><span>useRefEffect<\/span><span>( ( <\/span><span>element<\/span><span> ) <\/span><span>=&gt;<\/span><span> {<\/span><\/span>\n<span class=\"line\"><span>\t<\/span><span>const<\/span><span> { <\/span><span>defaultView<\/span><span> } = <\/span><span>element<\/span><span>.<\/span><span>ownerDocument<\/span><span>;<\/span><\/span>\n<span class=\"line\"><span>\t<\/span><span>const<\/span><span> <\/span><span>update<\/span><span> = () <\/span><span>=&gt;<\/span><span> <\/span><span>setWidth<\/span><span>( <\/span><span>defaultView<\/span><span>.<\/span><span>innerWidth<\/span><span> );<\/span><\/span>\n<span class=\"line\"><span>\t<\/span><span>update<\/span><span>();<\/span><\/span>\n<span class=\"line\"><span>\t<\/span><span>defaultView<\/span><span>.<\/span><span>addEventListener<\/span><span>( <\/span><span>'resize'<\/span><span>, <\/span><span>update<\/span><span> );<\/span><\/span>\n<span class=\"line\"><span>\t<\/span><span>return<\/span><span> () <\/span><span>=&gt;<\/span><span> <\/span><span>defaultView<\/span><span>.<\/span><span>removeEventListener<\/span><span>( <\/span><span>'resize'<\/span><span>, <\/span><span>update<\/span><span> );<\/span><\/span>\n<span class=\"line\"><span>}, [] );<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span>const<\/span><span> <\/span><span>blockProps<\/span><span> = <\/span><span>useBlockProps<\/span><span>( { <\/span><span>ref<\/span><span> } );<\/span><\/span><\/code><\/pre>\n<\/div>\n<p class=\"wp-block-paragraph\">Two things to notice:<\/p>\n<ul class=\"wp-block-list\">\n<li><code>element.ownerDocument<\/code> is whatever document the block is rendered into \u2014 the iframe\u2019s document when iframed, the admin document when not. <code>ownerDocument.defaultView<\/code> is that document\u2019s window. Code written this way is <em>context-agnostic<\/em>: it doesn\u2019t care whether the iframe exists.<\/li>\n<li><code>useRefEffect<\/code> (from <code>@wordpress\/compose<\/code>) instead of <code>useRef<\/code> + <code>useEffect<\/code>: it re-runs the callback when the ref changes, so if the block ever moves between documents, your listeners re-attach to the right window.<\/li>\n<\/ul>\n<h3 class=\"wp-block-heading\">2. \u201cClose on outside click\u201d and other document-level events<\/h3>\n<p class=\"wp-block-paragraph\">This one is my favorite because it fails <em>weirdly<\/em>. A dropdown that closes when you click outside, implemented the way every React tutorial teaches it:<\/p>\n<div class=\"wp-block-kevinbatdorf-code-block-pro\"><span><span>JavaScript<\/span><\/span><span class=\"code-block-pro-copy-button\"><\/p>\n<pre class=\"code-block-pro-copy-button-pre\"><textarea class=\"code-block-pro-copy-button-textarea\" readonly>\/\/ <img decoding=\"async\" alt=\"\u274c\" class=\"wp-smiley\" src=\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/274c.png\"> Broken in the iframed editor\nuseEffect( () =&gt; {\n\tconst closeOnOutsideClick = ( event ) =&gt; {\n\t\tif ( ! containerRef.current.contains( event.target ) ) {\n\t\t\tsetIsOpen( false );\n\t\t}\n\t};\n\tdocument.addEventListener( 'click', closeOnOutsideClick );\n\treturn () =&gt; document.removeEventListener( 'click', closeOnOutsideClick );\n}, [] );<\/textarea><\/pre>\n<p><\/span><\/p>\n<pre class=\"shiki dark-plus\"><code><span class=\"line\"><span>\/\/ \u274c Broken in the iframed editor<\/span><\/span>\n<span class=\"line\"><span>useEffect<\/span><span>( () <\/span><span>=&gt;<\/span><span> {<\/span><\/span>\n<span class=\"line\"><span>\t<\/span><span>const<\/span><span> <\/span><span>closeOnOutsideClick<\/span><span> = ( <\/span><span>event<\/span><span> ) <\/span><span>=&gt;<\/span><span> {<\/span><\/span>\n<span class=\"line\"><span>\t\t<\/span><span>if<\/span><span> ( ! <\/span><span>containerRef<\/span><span>.<\/span><span>current<\/span><span>.<\/span><span>contains<\/span><span>( <\/span><span>event<\/span><span>.<\/span><span>target<\/span><span> ) ) {<\/span><\/span>\n<span class=\"line\"><span>\t\t\t<\/span><span>setIsOpen<\/span><span>( <\/span><span>false<\/span><span> );<\/span><\/span>\n<span class=\"line\"><span>\t\t}<\/span><\/span>\n<span class=\"line\"><span>\t};<\/span><\/span>\n<span class=\"line\"><span>\t<\/span><span>document<\/span><span>.<\/span><span>addEventListener<\/span><span>( <\/span><span>'click'<\/span><span>, <\/span><span>closeOnOutsideClick<\/span><span> );<\/span><\/span>\n<span class=\"line\"><span>\t<\/span><span>return<\/span><span> () <\/span><span>=&gt;<\/span><span> <\/span><span>document<\/span><span>.<\/span><span>removeEventListener<\/span><span>( <\/span><span>'click'<\/span><span>, <\/span><span>closeOnOutsideClick<\/span><span> );<\/span><\/span>\n<span class=\"line\"><span>}, [] );<\/span><\/span><\/code><\/pre>\n<\/div>\n<p class=\"wp-block-paragraph\">In the iframed editor, clicks inside the canvas happen in the <strong>iframe\u2019s<\/strong> document. They never bubble to the admin document, so the listener never fires. The result: click another block in the canvas and the dropdown stays open \u2014 but click the admin sidebar and it closes. Same code, same block, works perfectly in the non-iframed editor. This is the kind of bug report you\u2019ll get from users that \u201ccan\u2019t be reproduced\u201d \u2014 because whoever tested it happened to have a v2 block sitting in their post, which quietly dropped the iframe and made everything work.<\/p>\n<p class=\"wp-block-paragraph\">Fix: same principle, attach to <code>element.ownerDocument<\/code> instead of <code>document<\/code> (<a href=\"https:\/\/github.com\/ryanwelcher\/iframe-editor-examples\/blob\/trunk\/src\/click-outside-fixed\/edit.js#L32\" target=\"_blank\" rel=\"noopener\">see the plugin for the full <code>useRefEffect<\/code> version<\/a>).<\/p>\n<h3 class=\"wp-block-heading\">3. Editor styles enqueued into the wrong document<\/h3>\n<p class=\"wp-block-paragraph\">If you\u2019re styling your block\u2019s editor experience with <code>enqueue_block_editor_assets<\/code>, those styles load in the <strong>admin page<\/strong> \u2014 outside the iframe. They silently stop applying the moment the canvas is iframed:<\/p>\n<div class=\"wp-block-kevinbatdorf-code-block-pro\"><span><span>PHP<\/span><\/span><span class=\"code-block-pro-copy-button\"><\/p>\n<pre class=\"code-block-pro-copy-button-pre\"><textarea class=\"code-block-pro-copy-button-textarea\" readonly>\/\/ <img decoding=\"async\" alt=\"\u274c\" class=\"wp-smiley\" src=\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/274c.png\"> Loads in the admin page \u2014 never reaches the iframed canvas.\nfunction myplugin_enqueue_editor_styles() {\n\twp_enqueue_style( 'myplugin-editor', plugins_url( 'editor.css', __FILE__ ) );\n}\nadd_action( 'enqueue_block_editor_assets', 'myplugin_enqueue_editor_styles' );<\/textarea><\/pre>\n<p><\/span><\/p>\n<pre class=\"shiki dark-plus\"><code><span class=\"line\"><span>\/\/ \u274c Loads in the admin page \u2014 never reaches the iframed canvas.<\/span><\/span>\n<span class=\"line\"><span>function<\/span><span> <\/span><span>myplugin_enqueue_editor_styles<\/span><span>() {<\/span><\/span>\n<span class=\"line\"><span>\t<\/span><span>wp_enqueue_style<\/span><span>( <\/span><span>'myplugin-editor'<\/span><span>, <\/span><span>plugins_url<\/span><span>( <\/span><span>'editor.css'<\/span><span>, <\/span><span>__FILE__<\/span><span> ) );<\/span><\/span>\n<span class=\"line\"><span>}<\/span><\/span>\n<span class=\"line\"><span>add_action<\/span><span>( <\/span><span>'enqueue_block_editor_assets'<\/span><span>, <\/span><span>'myplugin_enqueue_editor_styles'<\/span><span> );<\/span><\/span><\/code><\/pre>\n<\/div>\n<p class=\"wp-block-paragraph\">The fix is to register editor styles through <code>block.json<\/code>, which WordPress injects <strong>into<\/strong> the canvas document, iframed or not:<\/p>\n<div class=\"wp-block-kevinbatdorf-code-block-pro\"><span><span>JSON<\/span><\/span><span class=\"code-block-pro-copy-button\"><\/p>\n<pre class=\"code-block-pro-copy-button-pre\"><textarea class=\"code-block-pro-copy-button-textarea\" readonly>{\n\t\"editorStyle\": \"file:.\/index.css\"\n}<\/textarea><\/pre>\n<p><\/span><\/p>\n<pre class=\"shiki dark-plus\"><code><span class=\"line\"><span>{<\/span><\/span>\n<span class=\"line\"><span>\t<\/span><span>\"editorStyle\"<\/span><span>: <\/span><span>\"file:.\/index.css\"<\/span><\/span>\n<span class=\"line\"><span>}<\/span><\/span><\/code><\/pre>\n<\/div>\n<p class=\"wp-block-paragraph\">(<code>add_editor_style()<\/code> also gets copied into the iframe, if you need theme-level editor styles.)<\/p>\n<p class=\"wp-block-paragraph\">The demo plugin makes this visual: the same block carries a green banner from <code>editorStyle<\/code> and a red banner from <code>enqueue_block_editor_assets<\/code>. Count the banners \u2014 two means no iframe, one means you\u2019re iframed.<\/p>\n<h3 class=\"wp-block-heading\">4. Stale CSS written for the leaky editor<\/h3>\n<p class=\"wp-block-paragraph\">The section above is about CSS loading into the wrong document. This one is the sneakier inverse: the stylesheet loads into the <em>right<\/em> document \u2014 injected straight into the canvas, exactly as intended \u2014 and still gets it wrong, because of what it was written to describe. These are the rules that quietly stop matching, or start over-matching, once the canvas becomes its own document. It\u2019s the code that\u2019s been sitting in themes and plugins for years, \u201cworking,\u201d right up until the iframe is enforced.<\/p>\n<h4 class=\"wp-block-heading\"><strong>Selectors keyed on admin body classes<\/strong><\/h4>\n<p class=\"wp-block-paragraph\">The most common one, and it fails exactly like the \u201cclose on outside click\u201d bug \u2014 silently.<\/p>\n<div class=\"wp-block-kevinbatdorf-code-block-pro\"><span><span>CSS<\/span><\/span><span class=\"code-block-pro-copy-button\"><\/p>\n<pre class=\"code-block-pro-copy-button-pre\"><textarea class=\"code-block-pro-copy-button-textarea\" readonly>\/* <img decoding=\"async\" alt=\"\u274c\" class=\"wp-smiley\" src=\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/274c.png\"> The canvas body no longer carries these classes *\/\n.wp-admin .my-block { padding: 2rem; }\nbody.block-editor-page .my-block__title { font-size: 2rem; }<\/textarea><\/pre>\n<p><\/span><\/p>\n<pre class=\"shiki dark-plus\"><code><span class=\"line\"><span>\/* \u274c The canvas body no longer carries these classes *\/<\/span><\/span>\n<span class=\"line\"><span>.wp-admin<\/span><span> <\/span><span>.my-block<\/span><span> { <\/span><span>padding<\/span><span>: <\/span><span>2rem<\/span><span>; }<\/span><\/span>\n<span class=\"line\"><span>body.block-editor-page<\/span><span> <\/span><span>.my-block__title<\/span><span> { <\/span><span>font-size<\/span><span>: <\/span><span>2rem<\/span><span>; }<\/span><\/span><\/code><\/pre>\n<\/div>\n<p class=\"wp-block-paragraph\">Inside the iframe, the canvas <code>&lt;body&gt;<\/code> is a clean document \u2014 no <code>wp-admin<\/code>, no <code>block-editor-page<\/code>. The selector matches nothing and your editor styling just evaporates. Same block, same stylesheet, works perfectly in the non-iframed editor.<\/p>\n<div class=\"wp-block-kevinbatdorf-code-block-pro\"><span><span>CSS<\/span><\/span><span class=\"code-block-pro-copy-button\"><\/p>\n<pre class=\"code-block-pro-copy-button-pre\"><textarea class=\"code-block-pro-copy-button-textarea\" readonly>\/* <img decoding=\"async\" alt=\"\u2705\" class=\"wp-smiley\" src=\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/2705.png\"> Scope to the block, not the admin chrome *\/\n.my-block { padding: 2rem; }\n.my-block__title { font-size: 2rem; }<\/textarea><\/pre>\n<p><\/span><\/p>\n<pre class=\"shiki dark-plus\"><code><span class=\"line\"><span>\/* \u2705 Scope to the block, not the admin chrome *\/<\/span><\/span>\n<span class=\"line\"><span>.my-block<\/span><span> { <\/span><span>padding<\/span><span>: <\/span><span>2rem<\/span><span>; }<\/span><\/span>\n<span class=\"line\"><span>.my-block__title<\/span><span> { <\/span><span>font-size<\/span><span>: <\/span><span>2rem<\/span><span>; }<\/span><\/span><\/code><\/pre>\n<\/div>\n<p class=\"wp-block-paragraph\"><code>.editor-styles-wrapper<\/code> <em>does<\/em> still wrap the canvas content inside the iframe, so <code>.editor-styles-wrapper .my-block<\/code> keeps working if you need genuinely editor-only styling \u2014 but the admin ancestor was almost never necessary in the first place.<\/p>\n<h4 class=\"wp-block-heading\"><strong>Offsets that compensate for admin chrome<\/strong><\/h4>\n<div class=\"wp-block-kevinbatdorf-code-block-pro\"><span><span>CSS<\/span><\/span><span class=\"code-block-pro-copy-button\"><\/p>\n<pre class=\"code-block-pro-copy-button-pre\"><textarea class=\"code-block-pro-copy-button-textarea\" readonly>\/* <img decoding=\"async\" alt=\"\u274c\" class=\"wp-smiley\" src=\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/274c.png\"> Subtracting the admin sidebar and adminbar from the viewport *\/\n.my-fullwidth { width: calc( 100vw - 160px ); } \/* 160px = admin menu *\/\n.my-toolbar   { position: fixed; top: 32px; }   \/* 32px = #wpadminbar *\/<\/textarea><\/pre>\n<p><\/span><\/p>\n<pre class=\"shiki dark-plus\"><code><span class=\"line\"><span>\/* \u274c Subtracting the admin sidebar and adminbar from the viewport *\/<\/span><\/span>\n<span class=\"line\"><span>.my-fullwidth<\/span><span> { <\/span><span>width<\/span><span>: <\/span><span>calc<\/span><span>( <\/span><span>100vw<\/span><span> - <\/span><span>160px<\/span><span> ); } <\/span><span>\/* 160px = admin menu *\/<\/span><\/span>\n<span class=\"line\"><span>.my-toolbar<\/span><span>   { <\/span><span>position<\/span><span>: <\/span><span>fixed<\/span><span>; <\/span><span>top<\/span><span>: <\/span><span>32px<\/span><span>; }   <\/span><span>\/* 32px = #wpadminbar *\/<\/span><\/span><\/code><\/pre>\n<\/div>\n<p class=\"wp-block-paragraph\">This is the flip side of the win from earlier: now that <code>100vw<\/code> resolves against the <em>canvas<\/em> instead of the admin page, there\u2019s no sidebar to subtract \u2014 so the <code>calc()<\/code> overshoots, and <code>top: 32px<\/code> pushes your toolbar below an admin bar that doesn\u2019t exist in this document.<\/p>\n<div class=\"wp-block-kevinbatdorf-code-block-pro\"><span><span>CSS<\/span><\/span><span class=\"code-block-pro-copy-button\"><\/p>\n<pre class=\"code-block-pro-copy-button-pre\"><textarea class=\"code-block-pro-copy-button-textarea\" readonly>\/* <img decoding=\"async\" alt=\"\u2705\" class=\"wp-smiley\" src=\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/2705.png\"> The canvas is the viewport now \u2014 no compensation needed *\/\n.my-fullwidth { width: 100vw; }\n.my-toolbar   { position: fixed; top: 0; }<\/textarea><\/pre>\n<p><\/span><\/p>\n<pre class=\"shiki dark-plus\"><code><span class=\"line\"><span>\/* \u2705 The canvas is the viewport now \u2014 no compensation needed *\/<\/span><\/span>\n<span class=\"line\"><span>.my-fullwidth<\/span><span> { <\/span><span>width<\/span><span>: <\/span><span>100vw<\/span><span>; }<\/span><\/span>\n<span class=\"line\"><span>.my-toolbar<\/span><span>   { <\/span><span>position<\/span><span>: <\/span><span>fixed<\/span><span>; <\/span><span>top<\/span><span>: <\/span><span>0<\/span><span>; }<\/span><\/span><\/code><\/pre>\n<\/div>\n<h4 class=\"wp-block-heading\"><strong>Specificity walls built to fight leakage<\/strong><\/h4>\n<div class=\"wp-block-kevinbatdorf-code-block-pro\"><span><span>CSS<\/span><\/span><span class=\"code-block-pro-copy-button\"><\/p>\n<pre class=\"code-block-pro-copy-button-pre\"><textarea class=\"code-block-pro-copy-button-textarea\" readonly>\/* <img decoding=\"async\" alt=\"\u274c\" class=\"wp-smiley\" src=\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/274c.png\"> Cranked up to beat leaking admin styles *\/\n.editor-styles-wrapper .my-block p {\n\tfont-family: Georgia, serif !important;\n\tline-height: 1.6 !important;\n\tbox-sizing: border-box !important;\n}<\/textarea><\/pre>\n<p><\/span><\/p>\n<pre class=\"shiki dark-plus\"><code><span class=\"line\"><span>\/* \u274c Cranked up to beat leaking admin styles *\/<\/span><\/span>\n<span class=\"line\"><span>.editor-styles-wrapper<\/span><span> <\/span><span>.my-block<\/span><span> <\/span><span>p<\/span><span> {<\/span><\/span>\n<span class=\"line\"><span>\t<\/span><span>font-family<\/span><span>: <\/span><span>Georgia<\/span><span>, <\/span><span>serif<\/span><span> <\/span><span>!important<\/span><span>;<\/span><\/span>\n<span class=\"line\"><span>\t<\/span><span>line-height<\/span><span>: <\/span><span>1.6<\/span><span> <\/span><span>!important<\/span><span>;<\/span><\/span>\n<span class=\"line\"><span>\t<\/span><span>box-sizing<\/span><span>: <\/span><span>border-box<\/span><span> <\/span><span>!important<\/span><span>;<\/span><\/span>\n<span class=\"line\"><span>}<\/span><\/span><\/code><\/pre>\n<\/div>\n<p class=\"wp-block-paragraph\">The iframe already stops admin CSS from leaking in \u2014 that\u2019s one of the reasons it\u2019s a good thing. These <code>!important<\/code>s and resets have no admin styles left to override, but they <em>do<\/em> now override the theme styles the iframe loads into the canvas. The result: your editor preview drifts <em>away<\/em> from the front end \u2014 the exact opposite of what the iframe is for.<\/p>\n<div class=\"wp-block-kevinbatdorf-code-block-pro\"><span><span>CSS<\/span><\/span><span class=\"code-block-pro-copy-button\"><\/p>\n<pre class=\"code-block-pro-copy-button-pre\"><textarea class=\"code-block-pro-copy-button-textarea\" readonly>\/* <img decoding=\"async\" alt=\"\u2705\" class=\"wp-smiley\" src=\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/2705.png\"> Let theme styles through; set only what your block truly owns *\/\n.my-block p { font-family: Georgia, serif; }<\/textarea><\/pre>\n<p><\/span><\/p>\n<pre class=\"shiki dark-plus\"><code><span class=\"line\"><span>\/* \u2705 Let theme styles through; set only what your block truly owns *\/<\/span><\/span>\n<span class=\"line\"><span>.my-block<\/span><span> <\/span><span>p<\/span><span> { <\/span><span>font-family<\/span><span>: <\/span><span>Georgia<\/span><span>, <\/span><span>serif<\/span><span>; }<\/span><\/span><\/code><\/pre>\n<\/div>\n<p class=\"wp-block-paragraph\">Two things to notice:<\/p>\n<ul class=\"wp-block-list\">\n<li>The pattern is the same as the JavaScript fixes: <strong>stop describing the admin, start describing your block.<\/strong> A selector that names <code>.wp-admin<\/code>, <code>#wpadminbar<\/code>, or <code>.block-editor-page<\/code> is reaching for chrome that isn\u2019t in the canvas document anymore.<\/li>\n<li>Most of these were <em>workarounds for problems the iframe solves.<\/em> Deleting them is usually the fix.<\/li>\n<\/ul>\n<h3 class=\"wp-block-heading\">5. Third-party libraries that assume one global context<\/h3>\n<p class=\"wp-block-paragraph\">The biggest real-world hazard. Masonry layouts, sliders, lightboxes, maps \u2014 a generation of libraries was written assuming there is exactly one <code>document<\/code>:<\/p>\n<div class=\"wp-block-kevinbatdorf-code-block-pro\"><span><span>JavaScript<\/span><\/span><span class=\"code-block-pro-copy-button\"><\/p>\n<pre class=\"code-block-pro-copy-button-pre\"><textarea class=\"code-block-pro-copy-button-textarea\" readonly>\/\/ Inside some-legacy-lib.js\nconst targets = document.querySelectorAll( selector ); \/\/ finds nothing in the iframe<\/textarea><\/pre>\n<p><\/span><\/p>\n<pre class=\"shiki dark-plus\"><code><span class=\"line\"><span>\/\/ Inside some-legacy-lib.js<\/span><\/span>\n<span class=\"line\"><span>const<\/span><span> <\/span><span>targets<\/span><span> = <\/span><span>document<\/span><span>.<\/span><span>querySelectorAll<\/span><span>( <\/span><span>selector<\/span><span> ); <\/span><span>\/\/ finds nothing in the iframe<\/span><\/span><\/code><\/pre>\n<\/div>\n<p class=\"wp-block-paragraph\">Your block calls the library, the library queries the admin document, finds zero matches, and silently does nothing. No error, no warning \u2014 the block just stops being enhanced.<\/p>\n<p class=\"wp-block-paragraph\">Your options, in order of preference:<\/p>\n<ul class=\"wp-block-list\">\n<li><strong>Pass elements, not selectors.<\/strong> If the library accepts an element (<code>lib.init( element )<\/code>), hand it the block\u2019s element from <code>useRefEffect<\/code> and you\u2019re usually fine.<\/li>\n<li><strong>Patch the library.<\/strong> For unmaintained dependencies, <a href=\"https:\/\/www.npmjs.com\/package\/patch-package\" rel=\"noopener\" target=\"_blank\"><code>patch-package<\/code><\/a> is the pragmatic answer: edit the module in <code>node_modules<\/code> to resolve <code>document<\/code>\/<code>window<\/code> from the element (<code>node.ownerDocument<\/code>), run <code>npx patch-package &lt;pkg&gt;<\/code>, commit the patch, add a <code>postinstall<\/code> script. The <a href=\"https:\/\/developer.wordpress.org\/block-editor\/reference-guides\/block-api\/block-api-versions\/block-migration-for-iframe-editor-compatibility\/\" rel=\"noopener\" target=\"_blank\">official migration guide<\/a> walks through a real patch for <code>@panzoom\/panzoom<\/code>.<\/li>\n<li><strong>Guard and bail.<\/strong> If the library is loaded inside the iframe (front-end scripts are), check for it on <code>defaultView<\/code> before using it: <code>if ( ! defaultView.jQuery ) return;<\/code><\/li>\n<\/ul>\n<h2 class=\"wp-block-heading\">So what does <code>apiVersion: 3<\/code> actually do?<\/h2>\n<p class=\"wp-block-paragraph\">Less than you might think \u2014 and that\u2019s the point. Declaring <code>\"apiVersion\": 3<\/code> in <code>block.json<\/code> doesn\u2019t change how your block renders; it\u2019s a <strong>signal that your block is iframe-ready<\/strong>. All core blocks have been on v3 since WordPress 6.3. For most blocks the migration is literally a one-line change\u2026 followed by the actual work: testing that nothing in your <code>edit<\/code> component (or the libraries it pulls in) touches the global <code>document<\/code>\/<code>window<\/code>.<\/p>\n<p class=\"wp-block-paragraph\">And to be clear about 7.1: the iframe will be enforced there <strong>regardless<\/strong> of <code>apiVersion<\/code>. Staying on v2 doesn\u2019t opt you out anymore \u2014 it just means you get the console warning <em>and<\/em> the breakage.<\/p>\n<h2 class=\"wp-block-heading\">How to test today<\/h2>\n<p class=\"wp-block-paragraph\">You don\u2019t need to wait for 7.1. What you\u2019re testing is that your block works in <strong>both states<\/strong> \u2014 iframed and not \u2014 because both will exist in the wild for a while yet.<\/p>\n<p class=\"wp-block-paragraph\"><strong>Iframed:<\/strong> install the <strong>Gutenberg plugin 22.6+<\/strong>. It enforces the iframe regardless of theme, so this is the fastest way to live in the future. <a href=\"https:\/\/wordpress.org\/news\/2026\/07\/wordpress-7-1-beta-1\/\" target=\"_blank\" rel=\"noopener\"><strong>7.1 Beta 1<\/strong> <\/a>does the same \u2014 I\u2019ve confirmed it forces the iframe on a classic theme, which is the merged behavior shipping in August.<\/p>\n<p class=\"wp-block-paragraph\"><strong>Not iframed:<\/strong> run <strong>WordPress 7.0 without the plugin<\/strong> and insert a v1\/v2 block alongside yours \u2014 the canvas drops the iframe on the fly. <a href=\"https:\/\/github.com\/ryanwelcher\/iframe-editor-examples\" target=\"_blank\" rel=\"noopener\">The companion plugin\u2019s <code>legacy-api-v2<\/code> block exists for exactly this.<\/a> Any theme will do: core 7.0 has no theme check in the iframe decision at all, so you don\u2019t need to hunt down a classic theme to reproduce this.<\/p>\n<p class=\"wp-block-paragraph\"><strong>Confirm which state you\u2019re in:<\/strong> <code>element.ownerDocument !== document<\/code>, or look for <code>iframe[name=\"editor-canvas\"]<\/code> in devtools.<\/p>\n<p class=\"wp-block-paragraph\"><strong>The Site Editor<\/strong> has been iframed for years \u2014 if your block already behaves there, you\u2019re most of the way home.<\/p>\n<p class=\"wp-block-paragraph\"><strong>The companion plugin<\/strong> ships a <code>wp-env<\/code> setup, an example override file that adds Gutenberg for enforced mode (copy it to <code>.wp-env.override.json<\/code>), and <strong>two Playground blueprints<\/strong> \u2014 one per state, so you can flip between iframed and not in two tabs without installing anything.<\/p>\n<h2 class=\"wp-block-heading\">The block author\u2019s checklist<\/h2>\n<ol class=\"wp-block-list\">\n<li>Set <code>\"apiVersion\": 3<\/code> in every <code>block.json<\/code>.<\/li>\n<li>Check your editor code for <code>window.<\/code> and <code>document.<\/code> \u2014 every hit is a suspect. Replace with <code>element.ownerDocument<\/code> \/ <code>.defaultView<\/code> via <code>useRefEffect<\/code>.<\/li>\n<li>Check for <code>enqueue_block_editor_assets<\/code> \u2014 move canvas-affecting styles to <code>editorStyle<\/code> in <code>block.json<\/code>.<\/li>\n<li>Check your editor CSS for <code>.wp-admin<\/code>, <code>#wpadminbar<\/code>, and <code>.block-editor-page<\/code> , admin chrome offsets and <code>!important<\/code><\/li>\n<li>Audit third-party libraries: pass elements not selectors, patch what you must.<\/li>\n<li>Test both <strong>states<\/strong>, not both themes: iframed (Gutenberg 22.6+ active) and not iframed (no plugin, v1\/v2 block inserted). <\/li>\n<li>Watch the console with <code>SCRIPT_DEBUG<\/code> on \u2014 the deprecation warnings tell you which registered blocks are still on v1\/v2.<\/li>\n<\/ol>\n<p class=\"wp-block-paragraph\">Resources<\/p>\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/make.wordpress.org\/core\/2026\/02\/24\/iframed-editor-changes-in-wordpress-7-0\/\" rel=\"noopener\" target=\"_blank\">Iframed Editor Changes in WordPress 7.0<\/a> \u2014 the dev note this all builds on<\/li>\n<li><a href=\"https:\/\/github.com\/WordPress\/gutenberg\/pull\/74042\" rel=\"noopener\" target=\"_blank\">Post editor: always iframe (#74042)<\/a> \u2014 the merged PR that makes 7.1 always-iframed, and the discussion behind the beta-feedback plan<\/li>\n<li><a href=\"https:\/\/make.wordpress.org\/core\/2026\/06\/19\/roadmap-to-7-1\/\" rel=\"noopener\" target=\"_blank\">Roadmap to 7.1<\/a> \u2014 the original block-themes-first plan, superseded by #74042<\/li>\n<li><a href=\"https:\/\/make.wordpress.org\/core\/2025\/11\/12\/preparing-the-post-editor-for-full-iframe-integration\/\" rel=\"noopener\" target=\"_blank\">Preparing the Post Editor for Full iframe Integration<\/a> \u2014 the 6.9 groundwork<\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/block-editor\/reference-guides\/block-api\/block-api-versions\/block-migration-for-iframe-editor-compatibility\/\" rel=\"noopener\" target=\"_blank\">Migrating Blocks for iframe Editor Compatibility<\/a> \u2014 the official migration guide<\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/block-editor\/reference-guides\/block-api\/block-api-versions\/\" rel=\"noopener\" target=\"_blank\">Block API Versions<\/a> \u2014 what each version means<\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/block-editor\/how-to-guides\/enqueueing-assets-in-the-editor\/\" target=\"_blank\" rel=\"noopener\">Enqueueing editor assets<\/a><\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/themes\/core-concepts\/including-assets\/#editor-stylesheets\" target=\"_blank\" rel=\"noopener\">Editor styles for themes<\/a><\/li>\n<li>Companion demo plugin: <a href=\"https:\/\/github.com\/ryanwelcher\/iframe-editor-examples\" rel=\"noopener\" target=\"_blank\">iframe-editor-examples<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>For years, the post editor has lived a double life. The Site Editor renders your blocks inside an iframe. The post editor \u2014 where most people actually spend their time \u2014 renders them directly in the admin page. That split ends with WordPress 7.1: the post editor canvas will always be an iframe, on every [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":11330,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-11329","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized"],"blog_post_layout_featured_media_urls":{"thumbnail":["https:\/\/4ksamachar.com\/wp-content\/uploads\/2026\/07\/7.1-iframe-block-check-GlNOtb-150x150.png",150,150,true],"full":["https:\/\/4ksamachar.com\/wp-content\/uploads\/2026\/07\/7.1-iframe-block-check-GlNOtb.png",652,652,false]},"categories_names":{"1":{"name":"Uncategorized","link":"https:\/\/4ksamachar.com\/?cat=1"}},"tags_names":[],"comments_number":"0","wpmagazine_modules_lite_featured_media_urls":{"thumbnail":["https:\/\/4ksamachar.com\/wp-content\/uploads\/2026\/07\/7.1-iframe-block-check-GlNOtb-150x150.png",150,150,true],"cvmm-medium":["https:\/\/4ksamachar.com\/wp-content\/uploads\/2026\/07\/7.1-iframe-block-check-GlNOtb-300x300.png",300,300,true],"cvmm-medium-plus":["https:\/\/4ksamachar.com\/wp-content\/uploads\/2026\/07\/7.1-iframe-block-check-GlNOtb-305x207.png",305,207,true],"cvmm-portrait":["https:\/\/4ksamachar.com\/wp-content\/uploads\/2026\/07\/7.1-iframe-block-check-GlNOtb-400x600.png",400,600,true],"cvmm-medium-square":["https:\/\/4ksamachar.com\/wp-content\/uploads\/2026\/07\/7.1-iframe-block-check-GlNOtb-600x600.png",600,600,true],"cvmm-large":["https:\/\/4ksamachar.com\/wp-content\/uploads\/2026\/07\/7.1-iframe-block-check-GlNOtb.png",652,652,false],"cvmm-small":["https:\/\/4ksamachar.com\/wp-content\/uploads\/2026\/07\/7.1-iframe-block-check-GlNOtb-130x95.png",130,95,true],"full":["https:\/\/4ksamachar.com\/wp-content\/uploads\/2026\/07\/7.1-iframe-block-check-GlNOtb.png",652,652,false]},"_links":{"self":[{"href":"https:\/\/4ksamachar.com\/index.php?rest_route=\/wp\/v2\/posts\/11329","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/4ksamachar.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/4ksamachar.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/4ksamachar.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/4ksamachar.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=11329"}],"version-history":[{"count":0,"href":"https:\/\/4ksamachar.com\/index.php?rest_route=\/wp\/v2\/posts\/11329\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/4ksamachar.com\/index.php?rest_route=\/wp\/v2\/media\/11330"}],"wp:attachment":[{"href":"https:\/\/4ksamachar.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=11329"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/4ksamachar.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=11329"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/4ksamachar.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=11329"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}