InnerBlocks with ACF blocks

My favorite new feature in Advanced Custom Fields 5.9 is support for InnerBlocks. This allows you to insert any block (core or custom) inside your ACF block.

Rather than having to create your own fields for Title, Content, and Button in your custom block, you can simply insert <InnerBlocks /> and use the block editor to build the content inside the block.

How to use InnerBlocks

When registering your ACF block, include 'jsx' => true in the supports array.

acf_register_block_type( array(
	'title'			=> __( 'About', 'client_textdomain' ),
	'name'			=> 'about',
	'render_template'	=> 'partials/blocks/about.php',
	'mode'			=> 'preview',
	'supports'		=> [
		'align'			=> false,
		'anchor'		=> true,
		'customClassName'	=> true,
		'jsx' 			=> true,
	]
));

In your template partial for the block, include <InnerBlocks /> where you would like the editable block area to appear.

$classes = ['block-about'];
if( !empty( $block['className'] ) )
    $classes = array_merge( $classes, explode( ' ', $block['className'] ) );

$anchor = '';
if( !empty( $block['anchor'] ) )
	$anchor = ' id="' . sanitize_title( $block['anchor'] ) . '"';

echo '<div class="' . join( ' ', $classes ) . '"' . $anchor . '>';
	echo '<div class="block-about__inner">';
		echo '<div class="block-about__content">';
			echo '<InnerBlocks />';
		echo '</div>';
		echo '<div class="block-about__image">';
			echo wp_get_attachment_image( get_field( 'image' ), 'be_thumbnail_l' );
		echo '</div>';
	echo '</div>';
echo '</div>';

Default value for InnerBlocks

It’s helpful to fill the InnerBlocks field with default content so the block looks correct when first inserted.

We’re building a new site for Nice Kicks, and they often need to highlight the release date and details for new sneakers. We built a Release Info block that uses InnerBlocks for the content area.

Rather than just having an empty white box when they first insert the block, we pre-populate it with default content using a block template.

Inside the block’s template file, create a $template array detailing which blocks should be added. Update <InnerBlocks /> to include the template.

You can find the available block attributes in a blocks.json file for each block in wp-includes/blocks.

$template = array(
	array('core/heading', array(
		'level' => 2,
		'content' => 'Title Goes Here',
	)),
    array( 'core/paragraph', array(
        'content' => '<strong>Colorway:</strong> <br /><strong>Style Code:</strong>  <br /><strong>Release Date:</strong> <br /><strong>MSRP:</strong> ',
    ) )
);

echo '<div class="' . join( ' ', $classes ) . '"' . $anchor . '>';
	echo '<InnerBlocks template="' . esc_attr( wp_json_encode( $template ) ) . '" />';
	$form_id = get_option( 'options_be_release_info_form' );
	if( !empty( $form_id ) && function_exists( 'wpforms_display' ) )
		wpforms_display( $form_id, true, true );
echo '</div>';

Placeholders instead of default content

In the above example we set the starting content for the block. If you were to publish the post without changing the text, the default content would appear in the blocks.

Alternatively, you can use the placeholder parameter to specify placeholder text. This will not be published, and when you select the field the placeholder text disappears.

I had two issues with placeholders, which is why I used default content instead:

  1. When you insert the block, the first block inside InnerBlocks is selected so its placeholder text is not visible. You have to insert the block then click outside the block to see the placeholder text.
  2. The placeholder field does not support HTML. In my use case, we used <strong> and <br /> to format the paragraph text, but that doesn’t work with the placeholder.

To use placeholders with the above example, change the $template to

$template = array(
	array('core/heading', array(
		'level' => 2,
		'placeholder' => 'Title Goes Here',
	)),
	array( 'core/paragraph', array(
		'placeholder' => '<strong>Colorway:</strong> <br /><strong>Style #:</strong>  <br /><strong>Release Date:</strong> <br /><strong>Price:</strong> ',
	) )
);

And this was the result:

Limit the blocks available in InnerBlocks

You can limit which blocks can be inserted into your InnerBlocks field using the allowedBlocks attribute.

Using the example above, I can limit the Release Info block to only include the heading and paragraph blocks:

$allowed_blocks = array( 'core/heading', 'core/paragraph' );

$template = array(
	array('core/heading', array(
		'level' => 2,
		'content' => 'Title Goes Here',
	)),
    array( 'core/paragraph', array(
        'content' => '<strong>Colorway:</strong> <br /><strong>Style Code:</strong>  <br /><strong>Release Date:</strong> <br /><strong>MSRP:</strong> ',
    ) )
);

echo '<div class="' . join( ' ', $classes ) . '"' . $anchor . '>';
	echo '<InnerBlocks allowedBlocks="' . esc_attr( wp_json_encode( $allowed_blocks ) ) . '" template="' . esc_attr( wp_json_encode( $template ) ) . '" />';
	$form_id = get_option( 'options_be_release_info_form' );
	if( !empty( $form_id ) && function_exists( 'wpforms_display' ) )
		wpforms_display( $form_id, true, true );
echo '</div>';

Template lock with InnerBlocks

You can also limit the flexibility by locking the template.

Adding templateLock="all" prevents inserting new blocks or removing/re-arranging current blocks

Adding templateLock="insert" prevents inserting new blocks or removing current blocks, but you can re-arrange the current blocks.

I recently built an Icon Heading block. The icon can be selected in the block settings sidebar using a dynamic dropdown field.

I used InnerBlocks for the heading itself so it would have all the standard options for customizing the heading (change block style, change heading type to h3). I used templateLock="all" so only the heading from my block template could be used in this block.

$classes = ['block-icon-heading'];
if( !empty( $block['className'] ) )
    $classes = array_merge( $classes, explode( ' ', $block['className'] ) );
if( !empty( $block['align'] ) )
    $classes[] = 'align' . $block['align'];

$anchor = '';
if( !empty( $block['anchor'] ) )
	$anchor = ' id="' . sanitize_title( $block['anchor'] ) . '"';

$template = array(
	array('core/heading', array(
		'level' => 2,
		'content' => 'Heading',
	)),
);

echo '<div class="' . join( ' ', $classes ) . '"' . $anchor . '>';
	$icon = get_field( 'dynamic_icon_category' );
	if( !empty( $icon ) )
		echo '<div class="icon-heading-wrap">' . be_icon( [ 'icon' => $icon, 'group' => 'category', 'size' => 38 ] ) . '</div>';
	echo '<InnerBlocks template="' . esc_attr( wp_json_encode( $template ) ) . '" templateLock="all" />';
echo '</div>';

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

Reader Interactions

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

Comments

  1. Andrew says

    Bill, I love your content/website, I would opening support affiliate links from you or other ways to support.

    A question I have, I am working with Innerblocks to make a columns block, I thought it would be excellent content to show the making of a columns custom block and a column acf block.

    • Bill Erickson says

      The issue you’ll have with creating a Columns block is that you can only have a single InnerBlocks in your custom block. So the way I would approach it is:

      • Create a “Column” block that contains an unrestricted InnerBlocks
      • Create a “Columns” block that’s limited to only inserting the column block.
      • Use CSS flex or grid to style the columns block based on the number of columns in it.
      • You might also want to unregister the core/columns block so your content editors don’t get confused with the two different column blocks.
  2. Robert Turner says

    Could you show us an example of how to do Blocks with ACF repeater groups and relationships?

    • Neil says

      Second this! Bill, I appreciate how your blog posts don’t just provide an example, but actually explain the logic behind the pieces of each example, so that others can understand the logic and create their own solution. So many “help” pieces only provide what is essentially a template to repeat the exact same thing and many are difficult to learn from.

      Refreshing.

      • Bill Erickson says

        The repeater field in an ACF block works exactly the same as it would with an ACF metabox. Here’s an example:

        On the new CultivateWP site we have a “Partners” block that has a repeater. Here’s how it looks like rendered (screenshot) and here’s what it looks like when you select the block (screenshot). Here’s the code.

        I find that it’s best to set the mode to “auto” so you can use the repeater in the main content area. If you set it to “preview” you’d have to manage your repeater in the sidebar, and it’s a bit too cramped over there.

        Inside the block, I’m calling get_field( 'partners' ) and then looping over the repeater field data.

        You could also use InnerBlocks inside this block too (I didn’t have any need for that). Somewhere in the block markup you’d put <InnerBlocks /> and in the block registration you’d set 'jsx' => true.

  3. Julian says

    Thanks so much. Your articles are some of the most helpful I’ve read about using Gutenberg. However, I still have problems successfully combining ACF and core blocks.

    For example, I might want to create a “card” block that contains a background image, an H3 heading, and a paragraph. I can, as you describe, put in a core/heading block as an inner block, set its initial state to H3, and even put in some placeholder text. So far, so good. But I can not see a way to fix that heading at H3 to stop people changing it to another heading level – thus messing up the card design.

    I could revert to using an ACF text field for the heading, which would solve that problem, but they would no longer be able to edit the text in “preview” mode. So depending on whether each text element was in a core block or an ACF field, they would have to keep jumping between “edit” and “preview”, giving them a disjointed experience of using the CMS.

    So I’m wondering if there is something a bit like template lock, but that instead of stopping people from inserting and rearranging blocks, it stops them from doing anything but editing the text.

    I don’t want to treat you as a support forum! But I think this might be an issue that many people encounter. If so, it could be helpful to mention it here.

    • Bill Erickson says

      You’re right, while you can use the template lock to prevent adding/removing/reordering the blocks used in that area, you can’t limit the options attached to those blocks. There isn’t an “h3” block, there’s a Heading block with the heading type as an option.

      You could build the card as you have described (h3, paragraph, background image option) but style it so an h2, h3, h4, h5, or h6 all looks the same. This would prevent the visual design of the block from breaking if someone used a different heading.

      Another approach is to make the “heading” a paragraph with a custom class that styles it to look like an h3. This is what I do in blocks where the large text looks like a heading but semantically should not be a heading. In a CTA block, I might have a paragraph with a class of .block-cta__title.

  4. Alekanto says

    Thanks a lot for your article. I think it’s the greatest showcase with explanation of ACF use cases I’ve seen so far.

    And with the additionnal example of template lock usage within a acf block, it can be super useful/powerful.

    Cheers from Italy.

  5. Casey Milne says

    Hey Bill, I’m trying to figure out how to specify a custom appender in an ACF block. Do you know if that’s possible? In React you have to get the component like InnerBlocks.ButtonBlockAppender and pass it to the renderAppender. I’ve tried passing various names into a JSON array but nothing I pass to renderAppender does anything, some things break the block others just leave me with the default appender. I think it’s so important to have a wider appender than the default.

    • Bill Erickson says

      I’m sorry but I don’t know what a “custom appender” is. You might try reaching out to ACF support.

      The new version of ACF supports building custom blocks “the WordPress way”, using `register_block_type()` and a block.json file. So if you have a WP tutorial on how to add an appender, it might work best with the new block.json approach in ACF.