Building a block with Advanced Custom Fields

2022 Update

There’s now a better way to build ACF blocks. See my post on Building ACF Blocks with block.json.

Read Now

The new block-based editor in WordPress provides a great new content editing experience, and it’s even more powerful with custom blocks.

The core blocks are built using React and JavaScript. You can build your own Gutenberg blocks with JavaScript as well. If you plan to distribute your block publicly as a plugin, I recommend building it with JavaScript to keep it lean and decrease dependencies on other libraries.

When building a single website with many unique blocks, it’s more effective to use a block-building plugin like Advanced Custom Fields. You can build blocks much faster by leveraging a tool you likely already use. This is especially useful if you’re a PHP developer with little JavaScript experience. ACF lets you build everything using PHP.

For more real-world examples of custom ACF blocks, see my article on Building a WordPress theme with Gutenberg.

What is Advanced Custom Fields

Advanced Custom Fields is a plugin for building additional content editing fields throughout the WordPress interface. You can build custom metaboxes, site options, user profile fields, and term metadata.

For a more detailed walkthrough of ACF and how it compares to similar plugins, see my Introduction to Custom Metaboxes. I plan to write more about ACF in the near future, so keep an eye on my Advanced Custom Fields category page.

ACF is the first popular metabox plugin to add support for building blocks. Other plugins are working on it as well – see CMB2 and Carbon Fields articles on Gutenberg compatibility – but ACF is leading the way.

Creating a block

There are three key parts to building a Gutenberg block with ACF:

  1. Register the block with PHP
  2. Build the block editor with the ACF user interface
  3. Describe how the block is rendered using PHP

I’ll walk you through each step required to build a simple “Team Member” block.

I’m storing the block’s markup and styles inside the theme to keep things simple, but you could also place the code in a core functionality plugin.

Register the block

We’ll use the acf_register_block_type() to register our custom block. I’ve provided a summary of the parameters below, and for more information see the ACF documentation.

You must have ACF 5.8 or later installed for this to work.

 * Register Blocks
 * @link
function be_register_blocks() {
	if( ! function_exists( 'acf_register_block_type' ) )

	acf_register_block_type( array(
		'name'			=> 'team-member',
		'title'			=> __( 'Team Member', 'clientname' ),
		'render_template'	=> 'partials/block-team-member.php',
		'category'		=> 'formatting',
		'icon'			=> 'admin-users',
		'mode'			=> 'auto',
		'keywords'		=> array( 'profile', 'user', 'author' )

add_action('acf/init', 'be_register_blocks' );

This is your unique name for the block. ACF automatically namespaces it for you, so my team-member name becomes acf/team-member in the database.


This is the title shown in the Gutenberg block editor.

Render Template

The template file used to render the block. The same template file will be used on the frontend and as the “Preview” view in the backend editor. This can either be a relative URL from the current theme, as shown above, or a full path to any file.

Alternatively, you can use the render_callback parameter to specify a function name that output’s the block’s HTML.


This determines in which section of the “Add Block” window it appears. The options provided by WP core are: common, formatting, layout, widgets, and embed.


Specify a dashicons icon to use, or a custom SVG icon.


This lets you control how the block is presented the Gutenberg block editor. The default is “auto” which renders the block to match the frontend until you select it, then it becomes an editor.

If set to “preview” it will always look like the frontend and you can edit content in the sidebar.

If set to “Edit” it appears like a metabox in the content area. The user can switch the mode by clicking the button in the top right corner, unless you specifically disable it with 'supports' => array( 'mode' => false ).


Up to three additional terms to use when a user is searching for the block. The name is always indexed so you don’t need to repeat it as a keyword here.

Build the block editor

Once the block has been registered, you can now go to Custom Fields in the admin and create your block editor. It works just like a standard metabox built in ACF.

Under the location rules, select “Block” is equal to “Team Member”.

Build the block markup

The final step is to write the markup for the block. I created a template partial in /partials/block-team-member.php , matching the render_template parameter I specified when registering the block. My template partial looks like:

 * Team Member block
 * @package      ClientName
 * @author       Bill Erickson
 * @since        1.0.0
 * @license      GPL-2.0+

$name = get_field( 'name' );
$title = get_field( 'title' );
$photo = get_field( 'photo' );
$description = get_field( 'description' );

echo '<div class="team-member">';
	echo '<div class="team-member--header">';
		if( !empty( $photo ) )
			echo wp_get_attachment_image( $photo['ID'], 'thumbnail', null, array( 'class' => 'team-member--avatar' ) );
		if( !empty( $name ) )
			echo '<h4>' . esc_html( $name ) . '</h4>';
		if( !empty( $title ) )
			echo '<h6 class="alt">' . esc_html( $title ) . '</h6>';
	echo '</div>';
	echo '<div class="team-member--content">' . apply_filters( 'ea_the_content', $description ) . '</div>';
echo '</div>';

Use the get_field() function to access the fields set in the block settings. You can then build the markup for your block based on your specific design requirements.

Building a Table of Contents block

In my article on Developing a Gutenberg Website I mentioned the Table of Contents block I built for a few clients. It dynamically lists all h2’s within the current article and links to each one.

I started by forking the WP Anchor Header plugin. My changes include:

  • Limiting it to only h2’s
  • Only running on posts that have the table-of-contents block.
  • Before automatically adding an ID to each heading, check if one exists already. Clients can manually specify the heading’s anchor link in the Gutenberg editor so we should respect that.
  • My ea_table_of_contents() function accepts a $count parameter. We use this to list the first few headings in the article on the homepage

The block doesn’t have any editable fields so I added a Message field describing how it works:

Here is the full code for the table of contents block.

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


      • Jim Thornton says

        Oh man, wonderful tut! Was spinning my wheels for a while with most recent stable ACF pro not being about to get “Show this field group if” … “Block” as an option.

        Looks like its in /my-account linked as “See All Versions” and that view all downloads link doesn’t work without the user token in the query param i think

  1. Lobsang says

    I am trying duplicate your block team member with ACF. I don’t see ACF input fields in the backend. Do I need to create template?

    • Bill Erickson says

      Yes, you need to create a render template so ACF knows what the output should look like.

      If you set the mode to ‘preview’ you’ll see the ACF fields in the right panel when the block is selected. If you set the mode to ‘edit’ you’ll see the ACF fields in the main Gutenberg editor when the block is selected.

  2. Elisa says

    Hi Bill,
    thank you for this precious article!

    If I proceed this way without a custom single.php template everything works fine and my custom block appears in the frontend.

    I’m trying to render my custom block in a custom single.php template.
    I recall the post content with this code:

    echo ”;
    echo do_shortcode(get_post_field(‘post_content’, $postid));
    echo ”;

    Everything but the custom block shows up.
    What am I doing wrong?

    Than you very much,

    • Bill Erickson says

      The raw HTML for Gutenberg blocks is not saved to post_content. Rather, a structured HTML comment is saved and then processed using a filter on the_content.

      If you view the source of your page, you’ll likely see the HTML comment for your custom block in there. I just ran your code on a site I’m building and a custom block looks like this:

      <!-- wp:acf/features {"id":"block_5c00618c8dde1","data":{"field_5c005e7e98a7c":{"5c00618f8dde2":{"field_5c005e9398a7d":"Client Carousel","field_5c005e9b98a7e":"check","field_5c005ea798a7f":"Unlike many attorney websites that feel impersonal...

      If you change your code to echo apply_filters( 'the_content', get_post_field( 'post_content', get_the_ID() ) ); you should see the custom block.

      Or replace that line with the_content(); which is the function traditionally used to output the post content.

      • Elisa says

        Tank you Bill,
        that’s perfect!!!!

        Just another question and then, I promise, I won’t bother you again 🙂

        When in archive page I try to get the excerpt it returns everything but my custom blocks…
        I’m trying to make a custom post type archive in this way:

        Any other thing I’m missing?

        Sorry, I’m just a poor graphic designer that’s trying to act as a developer 🙂
        … No future?

        • Bill Erickson says

          You’ll need to run the ‘the_content’ filter on the excerpt to convert the HTML block comments into actual markup.

          Like this: echo apply_filters( 'the_content', get_the_excerpt() );

          But I think the issue you see is a feature, not a bug. WordPress decided not to include dynamic blocks in the excerpt for a reason. After doing the above, you’ll probably want to strip all HTML markup out of the result, like this:

          // Get the excerpt
          $excerpt = get_the_excerpt();
          // Load dynamic blocks
          $excerpt = apply_filters( 'the_content', $excerpt );
          // Strip markup
          $excerpt = wp_strip_all_tags( $excerpt );
          // Limit to 55 words 
          $excerpt = wp_trim_words( $excerpt, 55 );
          // Display 
          echo '<p>' . $excerpt . '</p>';
  3. Elisa says

    thank you Bill!
    Finally that’s worked with a little modification:

    I had to do something like this:

    $pageid = get_the_id();
    $content_post = get_post($pageid);
    $excerpt = $content_post->post_content;
    $excerpt = apply_filters(‘the_content’, $excerpt);
    $excerpt = str_replace(‘]]>’, ‘]]>’, $excerpt);
    $excerpt = wp_strip_all_tags( $excerpt );
    $excerpt = wp_trim_words( $excerpt, 32 );

    and then echo $excerpt worked…

    Thank you very much for your help!

  4. Grégoire Noyelle says

    Hi Bill,

    Thank you very much for your article. Like you, I am a big fan of ACF but it generates a lot of queries in the database.

    I thought that with the integration in Gutenberg, ACF will use the same structure as the native one in Gutenberg. Unfortunately, this is not the case and a JSON format is generated in the post table. So I wonder if it’s not worse than before.
    In the long run, I really think I’m going to the React block to optimize the data display.

    What do you think?

    • Bill Erickson says

      You’re right. It’s not the best data storage mechanism. I wish we had an option to provide a core markup structure to use when saving the data, especially for future compatibility.

      If I make a call to action block and change themes, it just disappears if the partial no longer exists. If it was stored as actual markup in the post, the content would still be there.

      But on the flipside, the dynamic block structure is more resilient. Let’s say you build a block and use it on a few templates. Then the client asks for something which changes the underlying markup of the block. If the actual markup were stored in the posts, you’d get the “Unexpected Error” when the markup changes and have to delete the block and start over.

      The dynamic block structure lets you separate the content / metadata of the block from the display / markup of the block, which (IMHO) is important and missing from most Gutenberg blocks.

      There’s positives and negatives to both approaches. I’m looking forward to ACF continuing to evolve and hopefully provide us data storage options. I’m also excited to see new block builders appear that don’t have the technical debt of being a metabox plugin first. I think we’ll see a lot of innovation in this space over the next year.

      • GrĂ©goire Noyelle says

        Thanks a lot for your reply Bill. I have noticed the same bugs. But like you I’m very optimitic about this new ecosystem.

  5. Eric says

    Great article. I have one question. Will this automatically be accesible vía tbe REST API of do I need to create a function to make these custom blocks appear in the post’s API?

  6. Yvonne says

    Thanks for the great article. Would you use this on a production site even though the plugin version is in beta?

  7. Sasha Endoh says

    Hi Bill,

    Thanks for this article! I wonder if you’ve had a chance to look at performance when using ACF to create blocks. We usually don’t use their helper functions on the front end for that specific reason but I see you’re using their get_field function in the render code. Would love to hear your thoughts!

  8. Tim says

    Hi, thanks for a very helpful article. One thing I’m wondering, is how straightforward is it to use a custom acf block but then add ‘Style’ options in the sidebar?

    I’ve seen your tutorial for adding extra block styles to the core blocks, and can use the registerBlockStyle function to add a new Style option in the sidebar for my new acf block. But selecting the style in the sidebar does not add any css classes to my custom block, I guess for a non core block you’d need to write separate code to handle adding the selected classes?

    • Bill Erickson says

      Custom ACF Blocks do not support the block style feature yet, so you need to build the functionality yourself. Add a select field for Style Options and include all of your alternative style options in the dropdown.

      The added benefit to the ACF approach is your alternative styles can also use alternative markup, since the block is dynamically built using the ACF partial you define.

      With the Gutenberg block style options, it simply adds a class of `.is-style-{your style}` to the block. You can’t alter its markup based on the style selected.