Building ACF blocks with block.json

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.

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:

  1. Register every block that exists in the /blocks directory
  2. Cache the list of blocks so we aren’t traversing the file system on every pageload
  3. Register a stylesheet for each block
  4. Include any ACF field groups associated with that block
  5. 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 a style.
  • 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 normal init (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

Bill Erickson

Bill Erickson is the co-founder and lead developer at CultivateWP, a WordPress agency focusing on high performance sites for web publishers.

About Me
Ready to upgrade your website?

I build custom WordPress websites that look great and are easy to manage.

Let's Talk

Comments are closed. Continue the conversation with me on Twitter: @billerickson