ACF 6.0 includes a major improvement to the way blocks are built. It now supports using block.json, which aligns with WordPress core’s preferred method for block registration.
Why is this important? As WordPress ships new features for blocks, you can start using them right away. Blocks are registered “the WordPress way” so support all WP core features. You don’t have to wait for ACF to add support for a new feature.
What does this mean for my older blocks? Blocks built using acf_register_block_type() will continue working exactly as expected, and there’s no need to go back and update older code. I recommend using the new method for all future blocks though.
To start using this today, you’ll need to log into your ACF account and download the latest release candidate.
If you’re having problems with your blocks not showing up, first make sure you’re on ACF 6.0, then run your block.json file through a JSON validator to see if there are any issues. Unfortunately there’s no error messages when you have a typo in your JSON file.
Table of Contents
Create a block.json file
Each block will have a block.json file, so it works best to have a directory for each block. I recommend creating a /blocks/ directory in your theme or plugin to hold them.
Create a block.json file in your block-specific folder. Example: /blocks/tip/block.json
{
"name": "cwp/tip",
"title": "Recipe Tip",
"description": "",
"style": "file:./style.css",
"script": "",
"category": "cultivatewp",
"icon": "carrot",
"apiVersion": 2,
"keywords": [],
"acf": {
"mode": "preview",
"renderTemplate": "render.php"
},
"styles": [],
"supports": {
"align": false,
"anchor": false,
"alignContent": false,
"color": {
"text": false,
"background": true,
"link": false
},
"alignText": false,
"fullHeight": false
},
"attributes": {
}
}
Most of this will line up with settings for acf_register_block_type(), but there are some important things to note:
Custom prefixes in block name
For the name
you can now specify your own prefix, instead of all blocks being prefixed automatically with acf
. If you don’t add a prefix, the ACF one will be used.
Script
If you need to load a JavaScript file along with your block, use the script
parameter to pass the script handle: "script": "block-tip"
.
Make sure you also register that script and specify any dependencies.
/**
* Register block script
*/
function cwp_register_block_script() {
wp_register_script( 'block-tip', get_template_directory_uri() . '/blocks/tip/block-tip.js', [ 'jquery', 'acf' ] );
}
add_action( 'init', 'cwp_register_block_script' );
If you are leveraging the ACF JS API ( ex:window.acf.addAction
) you’ll need to include acf
as a dependency.
If you use a namespace other than “acf/” you need to use the full block name in the callback, so: render_block_preview/type=cwp/tip
Style
The style
parameter lets you specify a stylesheet to include with this block. There are two ways this can be used.
"style": "file:./style.css"
This will load the actual CSS directly in the document’s head. This means the CSS file is not loaded as a separate request, decreasing the initial page load time (pro), but also that the CSS file isn’t browser cached, slightly increasing the subsequent page load time (con).
Place your style.css file inside the block directory (ex: /wp-content/themes/my-theme/blocks/tip/style.css).
"style": "block-tip"
This will run wp_enqueue_style( 'block-tip' )
to load the CSS file normally, with the opposite pros/cons listed above. Elsewhere in your theme/plugin you should have wp_register_style( 'block-tip', get_template_directory_uri() . '/blocks/tip/style.css' )
Quick rant about styles:
If you are using a FSE theme (“block theme”) then the style loading will work exactly as you expect. The CSS files and inline styles will only load if that page contains the block.
If you’re like me and building “classic” PHP based themes, WordPress loads every registered block style in the header, regardless of whether that block exists on the page. You can use the should_load_separate_core_block_assets
filter to tell WP to only load the ones that are required, but it waits until wp_footer
to load the CSS, causing big CLS issues which makes this feature useless.
The WP core argument (as I understand it) is that we don’t know exactly which blocks are on the page currently. They could be in the post content, or in reusable blocks, or in block-based widget areas, or other block-based features. While that is all true, I would’ve preferred loading the CSS files we do know are in the post content in the header and loading any missing ones in the footer.
I had built my own CSS loader that figured out which blocks appeared on the page, but now I opt for using WP Rocket to strip unused CSS from the page.
Script
If you need to load a JavaScript file along with your block, use the script
parameter to pass the script handle: "script": "block-tip"
.
Make sure you also register that script and specify any dependencies.
/**
* Register block script
*/
function cwp_register_block_script() {
wp_register_script( 'block-tip', get_template_directory_uri() . '/blocks/tip/block-tip.js', [ 'jquery', 'acf' ] );
}
add_action( 'init', 'cwp_register_block_script' );
If you are leveraging the ACF JS API ( ex:window.acf.addAction
) you’ll need to include acf
as a dependency.
If you use a namespace other than “acf/” you need to use the full block name in the callback, so: render_block_preview/type=cwp/tip
Icon
You can specify a Dashicons icon to use, or include an actual SVG:
"icon": "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22.5 22.5'><defs><style>.a{fill:#222;}.b{fill:#1fa8af;}</style></defs><path class='a' d='M20.17,4a10.17,10.17,0,0,0-7-3.5L11.91.38h-.18a.64.64,0,0,0-.47.13A.68.68,0,0,0,11,.93L10.6,3.79a.48.48,0,0,0,.12.43.54.54,0,0,0,.44.18h.11l1.44.17c3.94.45,6.12,2.69,5.84,6a8.37,8.37,0,0,1-2.49,5.12A8.14,8.14,0,0,1,10,18.06l-.65,0H9.15a.8.8,0,0,0-.5.17.68.68,0,0,0-.25.44L8,21.5a.49.49,0,0,0,.12.42.57.57,0,0,0,.45.18h.17l1,0h.34a11.61,11.61,0,0,0,8.21-3.39,12.76,12.76,0,0,0,3.77-7.92A9.49,9.49,0,0,0,20.17,4Z'/><path class='b' d='M9.2,17h.15L10,17a7.61,7.61,0,0,0,3.64-.77L15,6.15a8.65,8.65,0,0,0-2.33-.57l-1-.12-1,7.35L9.23,7c-.11-.45-.33-.67-.65-.67H7.16c-.29,0-.5.22-.65.67L5.1,12.81,3.82,3a.55.55,0,0,0-.61-.55H.78a.36.36,0,0,0-.29.16A.6.6,0,0,0,.37,3a.5.5,0,0,0,0,.18L2.53,19.22a1.07,1.07,0,0,0,.23.6.64.64,0,0,0,.53.23H5.16c.37,0,.61-.21.73-.65l2-7.19Z'/></svg>",
ACF
ACF Specific settings will go in this array.
Use mode
to specify how the block is rendered in the block editor. The default is “auto” which renders the block to match the frontend until you select it, then it becomes an ACF field group editor. If set to “preview” it will always look like the frontend and you can edit ACF field group in the sidebar.
Use renderTemplate
to specify what PHP file will render this block. I typically have a render.php file in each block directory for consistency.
Alternatively, you can use renderCallback
to specify a PHP function that will output the block’s content.
Styles
Use styles
to specify an array of block type styles.
"styles": [
{ "name": "default", "label": "Default", "isDefault": true },
{ "name": "red", "label": "Red" },
{ "name": "green", "label": "Green" },
{ "name": "blue", "label": "Blue" }
],
Supports
Use supports
to specify what Gutenberg features this block supports. The default is false
for all items so you don’t need to specify all the items it doesn’t support, but I typically leave them all in there marked false
so I can easily toggle the ones I want to true
.
In this example, the only feature this block supports is a background color:
"supports": {
"align": false,
"anchor": false,
"alignContent": false,
"color": {
"text": false,
"background": true,
"link": false
},
"alignText": false,
"fullHeight": false
},
Attributes
You can set the default attributes for the block features. For instance, if the block supports a background color, you can have that block use the tertiary
color by default:
"attributes": {
"backgroundColor": {
"type": "string",
"default": "tertiary"
}
}
For more information, see the Block Editor Handbook article on Metadata in block.json.
Register your block
You should now have a folder in your theme/plugin with block.json, style.css, and render.php. The next step is to tell WordPress about your block using register_block_type()
.
In your plugin or theme’s functions.php file, add:
/**
* Load Blocks
*/
function cwp_load_blocks() {
register_block_type( get_template_directory() . '/blocks/tip/block.json' );
// Optional - register stylesheet if using Style Method 2 from above
wp_register_style( 'block-tip', get_template_directory_uri() . '/blocks/tip/style.css' );
}
add_action( 'init', 'cwp_load_blocks' );
That’s it! Now your custom block should be accessible in the block editor.
Advanced Usage
While the above works as a basic example, there are some ways we can improve this:
- Register every block that exists in the /blocks directory
- Cache the list of blocks so we aren’t traversing the file system on every pageload
- Register a stylesheet for each block
- Include any ACF field groups associated with that block
- Include any additional PHP files required by the block
Here’s the code I use in my themes, followed by a description of what it’s doing.
<?php
/**
* Blocks
*
* @package CultivateClient
* @author CultivateWP
* @since 1.0.0
* @license GPL-2.0+
**/
namespace Cultivate\Blocks;
/**
* Load Blocks
*/
function load_blocks() {
$theme = wp_get_theme();
$blocks = get_blocks();
foreach( $blocks as $block ) {
if ( file_exists( get_template_directory() . '/blocks/' . $block . '/block.json' ) ) {
register_block_type( get_template_directory() . '/blocks/' . $block . '/block.json' );
wp_register_style( 'block-' . $block, get_template_directory_uri() . '/blocks/' . $block . '/style.css', null, $theme->get( 'Version' ) );
if ( file_exists( get_template_directory() . '/blocks/' . $block . '/init.php' ) ) {
include_once get_template_directory() . '/blocks/' . $block . '/init.php';
}
}
}
}
add_action( 'init', __NAMESPACE__ . '\load_blocks', 5 );
/**
* Load ACF field groups for blocks
*/
function load_acf_field_group( $paths ) {
$blocks = get_blocks();
foreach( $blocks as $block ) {
$paths[] = get_template_directory() . '/blocks/' . $block;
}
return $paths;
}
add_filter( 'acf/settings/load_json', __NAMESPACE__ . '\load_acf_field_group' );
/**
* Get Blocks
*/
function get_blocks() {
$theme = wp_get_theme();
$blocks = get_option( 'cwp_blocks' );
$version = get_option( 'cwp_blocks_version' );
if ( empty( $blocks ) || version_compare( $theme->get( 'Version' ), $version ) || ( function_exists( 'wp_get_environment_type' ) && 'production' !== wp_get_environment_type() ) ) {
$blocks = scandir( get_template_directory() . '/blocks/' );
$blocks = array_values( array_diff( $blocks, array( '..', '.', '.DS_Store', '_base-block' ) ) );
update_option( 'cwp_blocks', $blocks );
update_option( 'cwp_blocks_version', $theme->get( 'Version' ) );
}
return $blocks;
}
/**
* Block categories
*
* @since 1.0.0
*/
function block_categories( $categories ) {
// Check to see if we already have a CultivateWP category
$include = true;
foreach( $categories as $category ) {
if( 'cultivatewp' === $category['slug'] ) {
$include = false;
}
}
if( $include ) {
$categories = array_merge(
$categories,
[
[
'slug' => 'cultivatewp',
'title' => __( 'CultivateWP', 'cultivate_textdomain' ),
'icon' => \cwp_icon( [ 'icon' => 'cultivatewp', 'group' => 'color', 'force' => true ] )
]
]
);
}
return $categories;
}
add_filter( 'block_categories_all', __NAMESPACE__ . '\block_categories' );
My get_blocks()
function scans the /block directory and makes an array of all my blocks. I store this as an option so we only have to do this once, and use the current theme’s version to bust the cache. So when I add a new block, I also bump the version number in style.css
The code does the following for each block:
- Call
register_block_type()
using the block.json file - Call
wp_register_style()
to register the block’s stylesheet. This will only load if the block.json file specifies astyle
. - If there’s an
init.php
file in the block directory, load that too. This is what I use for any additional PHP code I want to run independent of the block’s rendering. - I’m doing all of this on the
init
hook with a priority of 5 so I can use the normalinit
(priority 10) inside my init.php file. - The
acf/settings/load_json
filter tells ACF to look in my block directories for ACF JSON field group files.
Need Help?
Ask ACF Support 🙂. I’m closing comments on this post because I likely won’t have time to answer questions & troubleshoot issues, but ACF’s support team is excellent and very responsive.
I’ll also update this post with more information and resources as I find them.
Additional information
- The GitHub ticket Introducing the ACF PRO Block Versioning Developer Preview #654.