Home › Block and Plugin Tutorials › Block Editor › Block.json Version 3: A Guide for Plugin and Theme Developers
Block.json Version 3: A Guide for Plugin and Theme Developers
Blocks for everyone!
New to Block.json?
Please read my beginner-friendly guide to block.json.
Version 3 of the block.json schema was first introduced with WordPress 6.3, with the overall end goal of allowing the block editor to be iframed. The caveat being, if any blocks on the site were still using Version 2 or earlier, the editor would default to the old, non-iframe behavior.
TLDR: Block Developers
If you’re already using Block.json version 2, just update the ‘apiVersion’ to 3 and thoroughly test your block. This is the only change needed to use the new API if you’re already loading scripts and styles via Block.json.
Being in an iframe is beneficial because:
- They’re style-independent from the admin.
- They’re easier to manipulate and more responsive for simulating breakpoints.
- Block authors and theme authors can better style blocks in the editor without conflicting with admin styles, improving frontend rendering.
- It results in far fewer script and style conflicts and fewer hacks.
With WordPress Version 7.0, Version 3 of the block.json API will no longer be optional. With the next major version only a few months away at the time of this writing, it’s time to get prepared for Version 3 of the block.json API.
If you haven’t already updated to Version 3 of the block.json API schema, please read on. I’ll go over what Version 3 is in detail, why it’s causing some people a lot of trouble, and what the future is like for older non-maintained blocks.
What is Block.json Version 3?
Version 3 of block.json, as of this writing, enables an iframe to wrap around block content in the site and block editor. This separates the content area from admin styles, avoiding conflicts and style clashes. If version 3 is enabled, the block content will be neatly wrapped in an iframe, allowing theme and plugin authors a way to hook in and add their own customizations. This is particularly useful for theme developers who have long wished for better styling control in the block editor.
As of WordPress 6.3, the Post Editor is iframed if all registered blocks have aBlock API version 3or higher and no traditional metaboxes are registered.
Source: GitHub
The block.json file only comes into play for block developers. This is a version flag, so think of this as a “go” or “no go” panel of lights for all blocks. If all blocks have opted in, the iframe editor should be enabled, barring no custom field trickery.
Opting in to Version 3 is a Go / No-Go Situation
With WordPress 7.0, the “Go / No-Go” situation won’t be an option. All block editor assets will load within an iframe, and it’s up to each block author to test and ensure their block works inside an iframed editor.
In preparation for this move and to be proactive, WordPress 6.9 installs began showing a deprecation notice for blocks still using Version 2 or earlier. These appear in the browser’s developer console.
Block.json API v3 Deprecation Notice/Warning in WordPress 6.9
The SCRIPT_DEBUG constant must be set to true for the deprecation notice to show
Set SCRIPT_DEBUG in your wp-config.php file. Only do this locally or on a staging site.
How do you opt in to Block.json and v3?
All you have to do is change your Block.json’s API version to 3. It looks like this:
Sample Block.json With API Version of 3
Changing from version 2 to 3 is as simple as changing the version number for the apiVersion parameter. However, there are some caveats. If your block relies on third-party scripts, particularly scripts attached to a specific window, you might run into weird scoping issues. You will also need to set up a local environment to test your block in an iframe before shipping properly.
Test your block thoroughly
While opting in to Version 3 is easy, make sure your block still functions correctly, particularly if it interacts with third-party libraries.
What changes with version 3?
I’ll start with a quoted summary:
From WordPress 6.3 on, the post editor will be iframed if all registered blocks have a Block API version 3 or higher. Adding version 3 support means that the block should work inside an iframe, though the block may still be rendered outside the iframe if not all blocks support version 3.
The goal of version 3 is to load the post editor and site editor inside an iframe. Why an iframe? Please let me explain.
If you’ve ever tried to style in the block editor, you’ll notice that the mobile and tablet previews are rendered in an iframe. This makes styling inside an iframe possible, but not without hacks. We’ll be using two WordPress functions: wp_deregister_script and wp_register_script.
Here’s the hacky code we used to load styles in the iframe.
PHP
function dlx_add_block_stylesheets() {
// Register the main block style file.
wp_register_style(
'dlx_block_styles',
asset_path( 'wp_blocks.css' ),
array(),
dlx_SCRIPT_VERSION,
'all'
);
// Check to see if we need to inject styles into the preview modes.
$inject_styles = false;
// This is to check the file exists.
if ( file_exists( ABSPATH . WPINC . '/css/dist/block-editor/style.min.css') ) {
$inject_styles = true;
}
// If file exists, deregister core style so we can latch onto it.
if ( $inject_styles ) {
wp_deregister_style( 'wp-block-editor' );
// Re-register style with our theme block styles as a dependency.
wp_register_style(
'wp-block-editor',
esc_url( home_url( 'wp-includes/css/dist/block-editor/style.min.css' ) ),
array( 'dlx_block_styles' )
);
}
}
add_action( 'enqueue_block_editor_assets', 'dlx_add_block_stylesheets' );Do not use this code
This code is hacky and not ideal. It’s better now to use hook enqueue_block_assets.
Did I mention hacks? We’ve latched on to an existing style that loads in the iframe. First, we register our custom block styles using wp_register_style. After that, we check whether the stylesheet we want to latch onto exists. Lastly, we used wp_deregister_style to remove the core stylesheet and then re-register it with our styles as a dependency. WordPress will load its block editor stylesheet; when it does, we simply latch onto it as a dependency.
For this next hack, we needed to inject inline styles inside the iframe:
JavaScript
import Sidebar from "./sidebar";
const { registerPlugin } = wp.plugins;
const { PluginSidebar, PluginSidebarMoreMenuItem } = wp.editPost;
const { select } = wp.data;
let stylesIntroduced = false;
registerPlugin("landing-page-gutenberg-template", {
icon: (
<svg>
{ /* code here */ }
</svg>>
),
render: () => {
// We're piggy backing off a render method here and getting the style settings, adding to it, and updating the core store settings.
const settingStyles = select( 'core/block-editor' ).getSettings().styles; // This gets the current styles.
if ( settingStyles && ! stylesIntroduced ) {
// Add to the settingStyles array.
settingStyles.push( {
css: 'table { margin-bottom: 1.5rem; color: #5e5e5e; } th, td { font-family: "Poppins", "Poppins", -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-feature-settings: \'kern\', \'liga\', \'ss01\', \'ss02\';padding: 1rem; vertical-align: top; border-top: 1px solid #e6e6e6; }',
} )
// Update the settings.
wp.data.dispatch('core/block-editor').updateSettings( { styles: settingStyles } );
// Make sure we only do it once.
stylesIntroduced = true;
}
return null;
},
});This code runs in the render argument, which is used to do output to the screen. While in render, we get the block editor styles and add some inline CSS. Finally, we update the core styles.
So why am I showing you all this? Because the iframes make it incredibly difficult to style content in the block editor.
The issue with iframes
The issue with iframes is that they are essentially designed to be independent of the content it is in. Sure, we can style the iframe itself, but styling the contents within the iframe isn’t possible unless there is some way to hook into the iframe output. Easier said than done.
But iframes are here to say, and here’s why: its weakness as an independent element is also what makes iframes so great.
An iframe’s weakness as an independent element is also what makes it so great for rendering content in the block editor.
Why iframes are needed in the post and site editor
A list of reasons iframes are needed for the block editor
Since the block editor’s content will be wrapped in an iframe, it’s independent of any admin styles. In reverse, people loading scripts and styles in the block editor won’t affect the content unless explicitly added.
I encourage you to read about the rational behind iframes, as it is explained rather well in the linked article. It’s a bit of an older article, but many of its points still resonate today.
In general, [iframes] makes the lives of block and theme authors easier because styles from the front-end can be dropped in with very little, if nothing, to adjust.
@ellatrix on WordPress.org
Another point is that the responsive previews are more accurate as they can represent screensizes without many workarounds.
So, to recap, iframes are beneficial because:
- They’re style-independent from the admin.
- They’re easier to manipulate and more responsive for simulating breakpoints.
- Block authors and theme authors can better style blocks in the editor without conflicting with admin styles, improving frontend rendering.
- It results in far fewer script and style conflicts and fewer hacks.
So, with version 3, how do we get our scripts and styles in the iframe?
If you’re already using block.json to load your scripts and styles in, then you don’t need to worry about iframe placement. However, if you’re loading third-party dependencies that may need to load in the iframe, please pay attention to the following few sections.
I recommend testing your block plugin on a clean WordPress install with the default Twenty Twenty-Five block theme.
Your block, when inspected using the developer console, should be wrapped in an iframe.
Your Block Should Be Wrapped in an Iframe
As of WordPress 6.3, the Post Editor is iframed if all registered blocks have aBlock API version 3or higher and no traditional metaboxes are registered.
Source: GitHub
Problems encountered when switching to Version 3
Most blocks with version 1 and 2 blocks work fine with the iframe. There’s a small amount that might not, and it’s not as easy as scanning the plugin repo for a piece of code. It would involve going through every plugin and testing the blocks they have. At them moment, I don’t know of any block that doesn’t work, I would actually love to find one.
WordPress Core Team
I thought I’d briefly share some of the problems I and others are encountering with loading scripts/styles in iframes.
Missing styles in the editor
Some styles may be missing in the iframed editor. Make sure any style handles are included in block.json.
For example, I have a block that relies on the Dashicons style, which isn’t included in the iframed editor by default. In my case, I used enqueue_block_assets to register the style, and added dashicons as a dependency.
PHP
wp_register_style(
'wppic-badges',
Functions::get_plugin_url( 'dist/badges.css' ),
array( 'dashicons'),
Functions::get_plugin_version(),
'all'
);In other cases, the style may be registered, but not included in block.json. Make sure all style handles are loaded in the block editor.
Remember, editorStyle loads styles for only the block editor, whereas style will load for both the frontend and editor. Please read my article on block.json, which covers these parameters in detail.
JSON
"editorScript": "wp-plugin-info-card-block-js",
"editorStyle": [ "wp-plugin-info-card-block-editor-css", "wp-plugin-info-card-block-styles-css", "wp-plugin-info-card-block-editor-css-inline" ],
"style": [ "wppic-fancybox-css" ]Use hook enqueue_block_assets for iframe styles
Use hook enqueue_block_assets to load styles inside the iframe. Just note that the hook fires outside the iframe, within the iframe, and also on the frontend.
enqueue_block_assets loads in the editor, iframe, and also the front-end
Because a custom script can be loaded in the editor, iframe, and front-end, it’s best to perform object detection in JavaScript and prevent scripts from being output on the front-end.
Say, for example, I have a script that needs to skip execution if an iframe is detected:
JavaScript
document.addEventListener( 'DOMContentLoaded', function() {
const maybeIframe = document.querySelector( '.block-editor-iframe__body' );
if ( null !== maybeIframe ) {
return;
}
// Rest of the code.
} );As of this writing, the body tag in the iframe has a class of block-editor-iframe__body. We can use that tag to determine whether we’re in an iframe. In this case, I don’t want any code running inside the iframe, so I do a conditional check and return early if found.
You can also do the opposite, only executing JavaScript code inside the iframe.
JavaScript
document.addEventListener( 'DOMContentLoaded', function() {
const maybeIframe = document.querySelector( '.block-editor-iframe__body' );
if ( null === maybeIframe ) {
return;
}
// Execute iframe code
} );Since enqueue_block_assets can also load on the frontend, you can do an is_admin check and return early if found.
PHP
<?php
add_action(
'enqueue_block_assets',
function () {
/**
* Load any frontend scripts.
*/
if (! is_admin() ) {
// Enqueue any frontend scripts using wp_enqueue_script and wp_enqueue_style.
}
/**
* Load any block editor scripts.
*/
if ( is_admin() ) {
// Register any admin scripts using wp_register_script and wp_register_style.
}
}
);Third-party scripts are using the wrong window or document
If you have third-party scripts, they may be using the wrong window or document. In other words, they aren’t picking up the iframe’s window or document.
The trick is to use a ref to point to the correct window. Assuming the script is registered and not enqueued, you can assume a ref is pointing to the correct iframe.
The article mentioned has a simple example of how to do this:
JavaScript
import { __ } from '@wordpress/i18n';
import { useBlockProps } from '@wordpress/block-editor';
import { useRef, useEffect } from '@wordpress/element';
export default function Edit() {
const ref = useRef();
useEffect( () => {
const { ownerDocument } = ref.current;
const { defaultView } = ownerDocument;
defaultView.addEventListener( ... );
return () => {
defaultView.removeEventListener( ... );
};
}, [] );
const blockProps = useBlockProps( { ref } );
return (
<div { ...blockProps }>
Hello world!
</div>
);
}In my case, I had issues loading Fancybox and Fancybox’s Carousel inside the iframe. What I did was create a ref as mentioned in the article.
JavaScript
const screenshotsWrapper = useRef( null );I then assigned it to an element.
Adding a ref to an Existing Element
After that, I used a useCallback hook to load when the ref is ready. When it is, I grab the iframe document, create a custom event, and then dispatch on a timer ot make sure the DOM is rendered. To any listeners, I pass along the iframe’s document reference and window.
JavaScript
// Callback ref that fires when the element is mounted and DOM is ready.
const screenshotsWrapperRef = useCallback( ( node ) => {
// Store reference for potential cleanup.
screenshotsWrapper.current = node;
if ( node !== null ) {
// If screenshotsWrapper is in an iframe, fire off custom event.
const parentDocument = node.ownerDocument;
const newEvent = new CustomEvent( 'wppicFancyboxCarouselInit', {
detail: {
screenshotsWrapper: node,
document: parentDocument,
window,
},
} );
// Give component enough time to render so the event listener isn't fired before the component is mounted, aka, html visible on the page and can be parsed via the DOM.
setTimeout( () => {
parentDocument.dispatchEvent( newEvent );
}, 500 );
}
}, [ screenshotsWrapper ] );I’m then able to reference this window in another script.
JavaScript
import { Fancybox, Carousel } from '@fancyapps/ui';
document.addEventListener( 'wppicFancyboxCarouselInit', function( event ) {
const iframeWindow = event.detail.window;
const wrapper = event.detail.screenshotsWrapper;
iframeWindow.Carousel = Carousel; // Make Carousel available in the iframe window. This could also set it in non-iframe window, but that's fine for block.json v2 outliers.
// IMPORTANT: call Fancybox FROM iframe window
new iframeWindow.Carousel( wrapper, {
slidesPerPage: 1,
Dots: false,
infinite: false,
adaptiveHeight: false,
} );
} );In this case, I had to explicitly add Carousel to the iframe’s window to be able to initialize the carousel properly.
What I’ve found that doesn’t work, period.
- Using
domReady(NPM package) loads too early for iframe detection, and also doesn’t indicate that the block editor is loaded either. - Using
registerPluginloads items outside the iframe in my testing. - Using
SlotandFillto get components inside an iframe works, but there’s no way to conditionally load because there’s no programmatic way to determine if you’re inside an iframe. - There is no simple
isIframedetection. You’ll have to test the DOM or use object detection to determine whether your script loads correctly in the iframe.
enqueue_block_editor_assets vs enqueue_block_assets
As of WordPress version 6.3, you can use enqueue_block_assets to load your scripts in the iframe. Hook enqueue_block_assets will still load outside the iframe and inside the iframe for backward compatibility purposes. Before WordPress 6.3, enqueue_block_assets was not able to load anything in an iframe.
It is now recommended to use enqueue_block_editor_assets only for items that interact with the editor interface and block editor APIs. For all other scripts/styles for the editor, you should use enqueue_block_assets instead.
What about previous versions like WordPress 6.2? Or how would someone support an older version and the newest version that encourages all block developers to opt in to version 3?
As of WordPress 6.3, all assets added through theenqueue_block_assetsPHP action will also be enqueued in the iframed Editor. See #48286 for more details.
This is the primary method you should use to enqueue assets for user-generated content (blocks), and this hook fires both in the Editor and on the front end of your site. It should not be used to add assets intended for the Editor UI or to interact with Editor APIs.
Source: GitHub
So, for 6.3 and above, enqueue_block_assets will load on the frontend and in the iframe (if iframe wrapping is enabled). Pre 6.3 enqueue_block_assets was primarily used to enqueue assets needed for the block editor (and the frontend).
Reminder: there is no iframe if version 3 isn't supported
All block plugins will have to have opt-in to version 3 in Block.json in order for the content to be wrapped in an iframe. With WordPress 7.0, version 3 is mandatory.
A question posted on GitHub brings up a good point. Why would we use enqueue_block_assets if the scripts and styles are also loaded in the frontend? Simply put, you can easily wrap an is_admin in your enqueue_block_assets action to prevent any frontend loading.
Here’s an example I “borrowed” from GitHub to demonstrate only iframe loading (note the is_admin check):
PHP
/**
* Enqueue content assets but only in the Editor.
*/
function example_enqueue_editor_content_assets() {
if ( is_admin() ) {
wp_enqueue_script(
'example-editor-content-scripts',
plugins_url( 'constent-scripts.css', __FILE__ ) );
}
wp_enqueue_style(
'example-editor-content-styles',
plugins_url( 'constent-styles.css', __FILE__ ) );
}
}
}
add_action( 'enqueue_block_assets, 'example_enqueue_editor_content_assets' );The above example will load the scripts in both the block editor (since it is enqueued) and the iframe.
How can I use enqueue_block_assets for older versions of WordPress
Do I have a hack for you? I’m using a class-based structure for this example, but it should work if you change the methods to functions and prefix them appropriately.
This first code sample uses the init action to start things off:
PHP
public static function run() {
$self = new self();
add_action( 'init', array( $self, 'init' ) );
return $self;
}This is what I refer to as a class runner. I usually call it when the plugins_loaded action is fired.
From there, let’s move into the init callback (I left some rational in the comments):
PHP
public function init() {
// Enqueue block assets.
add_action( 'enqueue_block_editor_assets', array( $this, 'register_block_editor_scripts' ) ); // This enqueue's like normal.
add_action( 'enqueue_block_assets', array( $this, 'action_enqueue_block_assets' ) );
// Since 6.3, enqueue_block_assets runs in an iframe in the site editor. In 6.3,
// enqueue_block_editor_assets loads outside of editor iframe. Thus, you can check
// if any of your enqueued script have been registered, and if not,
// you're on 6.3 and you can enqueue them here. Otherwise, you're on a
// pre-6.3 version.
}You’ll notice I’m using both enqueue_block_editor_assets and enqueue_block_assets together. Why would I do this?
enqueue_block_editor_assetsstill works well for pre-6.3 and is also still used to load scripts in the editor context.enqueue_block_assetsloads first from my tests, loading on the frontend, editor, and iframe.
You can still use enqueue_block_editor_assets to load scripts and styles needed for the block editor
You should just not use it anymore to target content within the block editor content unless you use prefixes.
As one contributor mentioned on GitHub:
enqueue_block_assetsshould be used to load editor stylesheets and scripts. Please noteenqueue_block_assetscan run on the frontend if there is nois_admincheck.enqueue_block_editor_assetsshould include scripts and styles that run in the block editor without affecting any content styles.- If using
enqueue_block_editor_assets, you’ll want to prefix your CSS with:.editor-styles-wrapperor.wp-block*. This is to prevent style leakage.
Let’s move on to the rest of the code:
PHP
/**
* Enqueue block assets inside an iframe.
*/
public function enqueue_block_editor_iframe() {
if ( ! is_admin() ) {
return;
}
// We'll pick on a script that's enqueued using `enqueue_block_editor_assets`.
if ( wp_script_is( 'alerts-dlx-block', 'enqueued' ) ) {
// If this script was enqueued, it means `enqueue_block_editor_assets` has run, therefore we are not in an iframe.
// If the script is not enqueued, we're likely in the iframe, so we can enqueue or assets as normal.
return;
}
// Register styles for the iframe in 6.3.
wp_enqueue_style(
'alerts-dlx-block-editor',
Functions::get_plugin_url( 'dist/alerts-dlx-block-editor.css' ),
array(),
Functions::get_plugin_version(),
'all'
);
wp_enqueue_style(
'alerts-dlx-block-editor-styles',
Functions::get_plugin_url( 'build/alerts-dlx.css' ),
array( 'alerts-dlx-block-editor' ),
Functions::get_plugin_version(),
'all'
);
}
/**
* Enqueue scripts for the block editor.
*/
public function enqueue_block_editor_scripts_styles() {
// Enqueue scripts and styles that need to go in the iframe.
wp_enqueue_style(
'alerts-dlx-block-editor',
Functions::get_plugin_url( 'dist/alerts-dlx-block-editor.css' ),
array(),
Functions::get_plugin_version(),
'all'
);
wp_enqueue_style(
'alerts-dlx-block-editor-styles',
Functions::get_plugin_url( 'build/alerts-dlx.css' ),
array( 'alerts-dlx-block-editor' ),
Functions::get_plugin_version(),
'all'
);
wp_enqueue_style(
'alerts-dlx-block-editor-styles-lato',
Functions::get_plugin_url( 'dist/alerts-dlx-gfont-lato.css' ),
array(),
Functions::get_plugin_version(),
'all'
);
wp_enqueue_style(
'alerts-dlx-common',
Functions::get_plugin_url( 'dist/alerts-dlx-common.css' ),
array( 'alerts-dlx-block-editor-styles-lato' ),
Functions::get_plugin_version(),
'all'
);
wp_enqueue_script(
'alerts-dlx-block',
Functions::get_plugin_url( 'build/alerts-dlx.js' ),
array(),
Functions::get_plugin_version(),
true
);
wp_localize_script(
'alerts-dlx-block',
'alertsDlxBlock',
array(
'font_stylesheet' => Functions::get_plugin_url( 'dist/alerts-dlx-gfont-lato.css' ),
)
);
}I’ve created a nifty flowchart to demonstrate what’s happening:
Use Enqueue Status to Determine if We’re on Older Versions of WordPress.
The theory behind this is that enqueue_block_assets loads first, so it has a chance to register its scripts. After that, enqueue_block_editor_assets is run, which registers its scripts/styles.
When in an iframe, enqueue_block_editor_assets is not run. The only thing that should be called in an iframe is enqueue_block_assets. So a simple handle check will eliminate any redundancy, and set you up for iframes in the site editor and the block editor in the future when iframes apply to the post content block editor.
As mentioned, all blocks on the site must have opted in to version 3 in Block.json by the time WordPress 7.0 comes out
Conclusion
Getting scripts and styles into the post editor iframe is coming very shortly with WordPress 7.0 just around the corner.
Switching to version 3 is simple for simple blocks. It’s just a single flag. However, the caveats, backward compatibility, and the amount of community opt-in will certainly make this change rather ambitious but worthwhile.
That being said, what do you think of this API change? Are you excited to finally be able to load assets in the content iframe? Please let me know in the comments.
Like the tutorial you just read? There's more like it.
There's more where that came from. Enter your email and I'll send you more like it.












