Custom Gutenberg blocks with ACF Pro

written by: Jeff McNear

I should premise this article with the fact that I install Advanced Custom Fields Pro with almost every site I build. The free version of ACF is very powerful, but at least as of right now (6/5/2020) the ability to create a custom Gutenberg block requires the Pro version. If I weren’t already installing ACF I may take a closer look at the Block Lab plugin ( which facilitates the building of custom plugins in possibly an easier manner.

As of about a year ago ACF Pro includes an option to apply the “location” rule of an ACF group to a block

But if the block hasn’t been registered somewhere (in your theme or plugin) ACF has no block to find.

Similar to registering a post type in WP, the acf_register_block() function allows you to register a custom block type from your functions.php file. This function accepts an array of settings that you can use to customize your block including a name, description and more.
(added to the functions file of your active theme)

add_action('acf/init', 'my_acf_init');
function my_acf_init() {
	// check function exists
	if( function_exists('acf_register_block') ) {
			'name'		=> 'accordion',
			'title'		=> __('Accordion'),
			'description'	=> __('A custom call to action block.'),
			'render_template'	=> 'inc/blocks/accordion.php',
			'category'		=> 'formatting',
			'icon'		=> 'welcome-learn-more',
			// icon is a dashicon
			'keywords'	=> array( 'cta','call to action', 'plasterdog', 'custom' ),
			'enqueue_style'	=> get_stylesheet_directory_uri() . '/inc/blocks/accordion.css',
			'enqueue_script' => get_template_directory_uri() . '/inc/blocks/accordion.js',

you’ll need to tell ACF how to render your block. It’s essentially the same process you’re used to for displaying custom fields – only that your HTML + PHP is wrapped in a function
(added to the functions file of your active theme)

function my_acf_block_render_callback( $block ) {
	// convert name ("acf/accordion") into path friendly slug ("accordion")
	$slug = str_replace('acf/', '', $block['name']);
	// include a template part from within the "template-parts/block" folder for the block
	if( file_exists( get_theme_file_path("/inc/blocks/accordion-{$slug}.php") ) ) {
		include( get_theme_file_path("/inc/blocks/accordion-{$slug}.php") );


Once blocks have been registered and rendered ACF will see them

… and you can then create a field group and assign fields to it:

Breaking down the array that registers the block, first you name it

'name'		=> 'accordion',

give it a title

'title'		=> __('Accordion'),

then a description

'description'	=> __('A custom call to action block.'),

then provide the path to the php file which defines the block

'render_template' => 'inc/blocks/accordion.php',

which in this case reads like this

<!-- -->
<button class="accordion" style="background-color:<?php the_field('block_faq_background_color'); ?>; color:<?php the_field('block_faq_text_color'); ?>;;">
	<?php the_field('block_faq_accordion_teaser');?></button>
<div class="panel fade-in quarter">
 <?php the_field('block_faq_accordion_content');?>

then define which category of block

'category'		=> 'formatting',

the relevant icon (see:

'icon'		=> 'welcome-learn-more',

relevant keywords for your user to find them

‘keywords’ => array( ‘cta’,’call to action’, ‘plasterdog’, ‘custom’ ),

attaching a relevant stylesheet

'enqueue_style'	=> get_stylesheet_directory_uri() . '/inc/blocks/accordion.css',

which in this case reads

 /*--- see: ---*/
@-webkit-keyframes fadeIn { from { opacity:0; } to { opacity:1; } }
@-moz-keyframes fadeIn { from { opacity:0; } to { opacity:1; } }
@keyframes fadeIn { from { opacity:0; } to { opacity:1; } }
.fade-in {  opacity:0;  /* make things invisible upon start */
  -webkit-animation:fadeIn ease-in 1;  /* call our keyframe named fadeIn, use animattion ease-in and repeat it only 1 time */
  -moz-animation:fadeIn ease-in 1;
  animation:fadeIn ease-in 1;
 /* this makes sure that after animation is done we remain at the last keyframe value (opacity: 1)*/
  -webkit-animation-fill-mode:forwards; -moz-animation-fill-mode:forwards; animation-fill-mode:forwards;
/*---nsets the default duration ---*/
  -webkit-animation-duration:1s; -moz-animation-duration:1s; animation-duration:1s;}
.fade-in.quarter {-webkit-animation-delay: 0.1s; -moz-animation-delay: 0.1s; animation-delay: 0.1s; }  
.fade-in.half {-webkit-animation-delay: 0.5s; -moz-animation-delay: 0.5s; animation-delay: 0.5s; }
.fade-in.full {-webkit-animation-delay: 1.25s; -moz-animation-delay: 1.25s; animation-delay: 1.25s;}

 /* Style the buttons that are used to open and close the accordion panel */
.accordion {cursor: pointer;padding: 1em 2em;width: 100%;text-align: left;border: none;outline: none;transition: 0.4s;-webkit-border-radius: 10px;
border-radius: 10px;}
button.accordion {-webkit-box-shadow: 0 0 0 0 transparent;box-shadow: 0 0 0 0 transparent;text-shadow: 0 0 0 transparent;}
.accordion:hover{text-decoration: underline;}
.accordion:after {
    font-family: "Font Awesome 5 Free"; font-weight: 900; content:"\f0d7"; font-size:1.5em;
    float: right;
} {
    font-family: "Font Awesome 5 Free"; font-weight: 900; content:"\f0de"; font-size:1.5em;
    float: right;

/* Add a background color to the button if it is clicked on (add the .active class with JS), and when you move the mouse over it (hover) */
.active, .accordion:hover {
  background-color: #ccc;

/* Style the accordion panel. Note: hidden by default */
.panel {
  padding: 1em 2em;
  background-color: white;
  display: none;
  overflow: hidden;
  transition: max-height 0.9s ease-out;

attach scripts if desired

'enqueue_script' => get_template_directory_uri() . '/inc/blocks/accordion.js',

which in this case is

/* SOURCE: */
var acc = document.getElementsByClassName("accordion");
var i;
for (i = 0; i < acc.length; i++) {
  acc[i].addEventListener("click", function() {
    /* Toggle between adding and removing the "active" class,
    to highlight the button that controls the panel */

    /* Toggle between hiding and showing the active panel */
    var panel = this.nextElementSibling;
    if ( === "block") { = "none";
    } else { = "block";

and now you have a custom block interface

Some things to bear in mind:

  • I couldn’t get any kind of conditional code to work inside of these blocks, and according to this link I am not alone
  • You can get this all to work within a plugin, however you will still need to activate the ACF definitions either by importing a JSON version or including a PHP version in your theme
  • There are already many blocks included with core and countless plugins which augment the options
  • Block patterns are a better way to build a layout than with an elaborate custom block