Create an Accordion Block with Advanced Custom Fields

Damon Cook Avatar


accordion block with ACF

There are a lot of native blocks available in WordPress. However, there are a few common user interface patterns that are missing, like accordions and tabs. Of course, there are open requests for these blocks, but the implementation details have yet to be solidified. Today, we’re going to go over how to create an accordion block with Advanced Custom Fields (ACF) integration.

First, let’s chat about where custom blocks belong. There is always a debate on whether blocks should reside in the theme or a plugin. I recommend placing your custom blocks in a plugin. This helps make them portable and encourages you to develop with theme-switching in mind. If you include your blocks within the theme then you’re more likely to make them rely solely on the theme, and if the user switches then the blocks will disappear and they’ll likely be confused and disappointed.

Here are the steps we’ll follow:

  1. Create a Block Plugin with the @wordpress/create-block Package
  2. Reorganize the New Plugin for ACF Field Integration
  3. Build our Final Block with @wordpress/scripts

Final Accordion Block Plugin

Advanced Custom Fields

Create a Block Plugin with the @wordpress/create-block Package

Let’s start with scaffolding a custom plugin and our accordion block with the handy @wordpress/create-block package. The @wordpress/create-block package has several handy options you can pass when creating a plugin or block. Here is the full list:

-V, --version output the version number -t, --template <name> project template type name; allowed values: "static" (default), "es5", the name of an external npm package, or the path to a local directory --no-plugin scaffold block files only --namespace <value> internal namespace for the block name --title <value> display title for the block and the WordPress plugin --short-description <value> short description for the block and the WordPress plugin --category <name> category name for the block --wp-scripts enable integration with `@wordpress/scripts` package --no-wp-scripts disable integration with `@wordpress/scripts` package --wp-env enable integration with `@wordpress/env` package -h, --help output usage information --variant choose a block variant as defined by the template
Code language: TypeScript (typescript)

We’ll be using the --variant flag to make sure we get a dynamic block and we’ll also pass acf for our --category and --namespace.

Let’s generate our accordion plugin and subsequent block.

npx @wordpress/create-block acf-accordion-block --variant dynamic --category acf-blocks --namespace acf && cd acf-accordion-block
Code language: Nginx (nginx)

Running this command will give us a plugin with a block included and since we passed the --variant dynamic flag to create-block then we’ll get a dynamic block.

WordPress editor with newly created accordion block inserted
Barebones accordion block created with the @wordpress/create-block package.
Directory structure of newly created accordion plugin in VS Code
Directory structure of newly created accordion plugin in VS Code.

You can go ahead and activate the ACF Accordion Block plugin, create a new post and add the ACF Accordion Block to your post now. The block is registered and available but is far from finished.

Reorganize the plugin for ACF integration

ACF makes custom field registration and hookup a pretty simple task. Here are the ACF field types we’ll need:

  • Repeater – Accordion Item (accordion_item)
    • Sub-field – Text – Accordion Heading (accordion_heading)
    • Sub-field – WYSIWYG – Accordion Content (accordion_content)

First, be sure to download and activate the latest Advanced Custom Fields plugin.

File Renaming and Restructuring

Delete the following files, as we will not need them:

  • src/editor.scss
  • src/edit.js

Rename the following files:

  • src/index.js –> src/accordion.js
  • src/style.scss –> src/accordion.scss
  • src/template.php –> src/accordion.php

Register ACF Fields

With the ACF Pro plugin activated, we can now add our custom fields to our plugin using ACF’s acf/include_fields action. Create a new file called acf-fields.json in the acf-accordion-block plugin, grab the full field code from the final plugin repo and paste it into this new file.

Our ACF fields now exist in our plugin, but are not registered. Let’s register them and assign them directly to the acf/acf-accordion-block:

Add the following code to your plugin’sacf-accordion-block.php

/** * Register our block's fields into ACF. * * @return void */ function acf_accordion_block_register_include_fields() { $path = __DIR__ . '/acf-fields.json'; $field_json = json_decode( file_get_contents( $path ), true ); $field_json['location'] = array( array( array( 'param' => 'block', 'operator' => '==', 'value' => 'acf/accordion', // block.json name. ), ), ); $field_json['local'] = 'json'; $field_json['local_file'] = $path; acf_add_local_field_group( $field_json ); } add_action( 'acf/include_fields', 'acf_accordion_block_register_include_fields' );
Code language: PHP (php)

The two things to note are that the $path points to our acf-fields.json, and the 'location' is assigned to the acf/accordion. The acf/accordion needs to match the name of our block within the build/block.json file. Keep in mind that everything in the build/ directory is automatically built with @wordpress/scripts, which was installed as part of @wordpress/create-block when we created our plugin. Everything in the src/ directory is the source of our block.

The block.json is the heart of registering blocks and you can read all about it here:

Register a Custom Block Category

You’ll recall that when we generated our plugin we passed the --category acf. This assigned a custom category to our block in the block.json. However, the custom block category needs to be registered. Otherwise, it does not truly exist.

Add this to your plugin’s acf-accordion-block.php to register a custom acfcategory.

/** * Register a custom block category for our blocks. * * @link * * @param array $block_categories Existing block categories * @return array Block categories */ function acf_accordion_block_block_categories( $block_categories ) { $block_categories = array_merge( [ [ 'slug' => 'acf-blocks', 'title' => __( 'ACF Blocks', 'acf_accordion_block' ), 'icon' => '<svg viewBox="0 0 55 24" fill="none" xmlns=""><path d="M43.9986 23.8816H38.0521V0.0253448H53.9034V5.58064H43.9986V9.83762H53.334V15.2547H43.9986V23.8825V23.8816Z" fill="black"/><path opacity="0.05" d="M36.4832 13.8697H42.3772C41.5051 19.9417 36.3849 23.9574 30.1814 23.9574C23.3882 23.9574 17.8572 18.8809 17.8572 12.0448C17.843 10.4551 18.1521 8.879 18.7658 7.41239C19.3795 5.94579 20.2849 4.61924 21.4271 3.51334C23.7714 1.24304 26.9182 -0.00834104 30.1814 0.0320335C36.3275 0.0320335 41.5908 4.07879 42.3392 10.0536H36.4511C34.6807 3.2856 23.649 3.94741 23.649 12.0448C23.649 20.1432 34.8189 20.7398 36.4832 13.8716V13.8697Z" fill="black"/><path d="M35.2772 13.8697C34.266 17.2858 30.667 19.317 27.1244 18.4664C23.5798 17.6128 21.3588 14.187 22.0946 10.7047C22.8294 7.22146 26.2572 4.92655 29.8582 5.50758C31.3334 5.70738 32.6937 6.41247 33.7074 7.50273C34.408 8.22394 34.9337 9.0963 35.2442 10.0526H40.96C40.2116 4.06425 34.9337 0.0320875 28.8022 0.0320875C25.5386 -0.00942939 22.391 1.24129 20.0459 3.51144C18.903 4.61761 17.997 5.94473 17.3831 7.41208C16.7693 8.87942 16.4603 10.4563 16.4751 12.0468C16.4751 18.8829 21.9739 23.9574 28.8042 23.9574C35.0028 23.9574 40.1084 19.9418 40.996 13.8697H35.2763H35.2772Z" fill="black"/><path opacity="0.05" d="M17.5146 20.4109H9.2391L7.88629 23.8776H1.55337L11.245 0H15.4689L25.5459 23.8854H18.8597L17.5127 20.4109H17.5146ZM11.5914 14.5004L11.3841 15.0396H15.4017L15.2625 14.6347L13.3919 9.51446L11.5914 14.5004Z" fill="black"/><path d="M15.9476 20.4109H7.68573L6.33389 23.8776H0L9.69257 0H13.9165L23.9935 23.8854H17.3102L15.9476 20.4109ZM10.0381 14.5004L9.83174 15.0396H13.8493L13.7092 14.6347L11.8396 9.51446L10.039 14.5004H10.0381Z" fill="black"/></svg>', ] ], $block_categories, ); return $block_categories; } add_filter( 'block_categories_all', 'acf_accordion_block_block_categories' );
Code language: PHP (php)

The 'slug' => 'acf-blocks' is the crucial piece here. As long as this slug matches the category within the block.json then the block will be properly assigned to this new custom “ACF Blocks” category.

Call block.json Directly with register_block_type()

By default, the @wordpress/create-block uses register_block_type()‘s render_callback to assign the template.php output. However, we’re going to streamline this a bit and call the block’s build/block.json directly and let ACF’s "renderTemplate": "accordion.php" do the talking.

Before: acf-accordion-block.php

/** * Registers the block using the metadata loaded from the `block.json` file. * Behind the scenes, it registers also all assets so they can be enqueued * through the block editor in the corresponding context. * * @see */ function acf_accordion_block_init() { register_block_type( __DIR__ . '/build', array( 'render_callback' => 'acf_accordion_block_render_callback', ) ); } add_action( 'init', 'acf_accordion_block_init' ); /** * Render callback function. * * @param array $attributes The block attributes. * @param string $content The block content. * @param WP_Block $block Block instance. * * @return string The rendered output. */ function acf_accordion_block_render_callback( $attributes, $content, $block ) { ob_start(); require plugin_dir_path( __FILE__ ) . 'build/template.php'; return ob_get_clean(); }
Code language: PHP (php)

After: acf-accordion-block.php

/** * We register the block on init, earlier than acf/init, * so we can make sure we ask ACF to load this block's fields. * * @return void */ function acf_accordion_block_register() { register_block_type( dirname(__FILE__) . '/build/block.json' ); } add_action( 'init', 'acf_accordion_block_register', 5 );
Code language: PHP (php)

And now we need to update the block’s block.json to integrate ACF’s custom "acf" keyed object.

The updated src/block.json

{ "$schema": "", "apiVersion": 2, "name": "acf/accordion", "version": "0.1.0", "title": "ACF Accordion Block", "category": "acf-blocks", "icon": "menu-alt3", "description": "An accordion block with collapsible sections.", "supports": { "color": { "text": true, "background": false }, "mode": false }, "keywords": [ "accordion", "toggle", "expand" ], "script": "file:./accordion.js", "style": "file:./accordion.css", "acf": { "mode": "preview", "renderTemplate": "accordion.php" } }
Code language: JSON / JSON with Comments (json)

Here we’re telling ACF to use the Preview mode when displaying the block, and passing the accordion.php to render our block’s logic. We’ve also replaced the "editorStyles" with "script". This is where we’ll put our accordion’s JavaScript.

Add accordion.php Display Logic

The src/accordion.php will hold all of the logic to display our final block in both the editor and the front end.

Goes in the src/accordion.php

<?php /** * Accordion block. */ $wrapper_attributes = get_block_wrapper_attributes( [ 'class' => 'accordion' ] ); ?> <div <?php echo $wrapper_attributes; ?>> <?php if ( empty( get_field( 'accordion_item' ) ) ) : ?> <p class="acf-accordion-block-empty-state"><?php esc_html_e( 'Please add some content in the sidebar.', 'acf-accordion-block' ); ?></p> <?php endif; ?> <?php foreach ( get_field( 'accordion_item' ) as $accordion_item ) : $heading = $accordion_item['accordion_heading'] ? $accordion_item['accordion_heading'] : 'Your heading goes here'; $content = $accordion_item['accordion_content'] ? $accordion_item['accordion_content'] : 'Your content goes here...'; ?> <button class="accordion-header" type="button"> <span class="accordion-title"> <?php echo esc_html( $heading ); ?> <span class="accordion-icon"></span> </span> </button> <div class="accordion-content"> <h2 class="accordion-label"><?php echo esc_html( $heading ); ?></h2> <?php echo $content; ?> </div><!-- .accordion-content --> <?php endforeach; ?> </div><!-- .accordion -->
Code language: JavaScript (javascript)

Add Accordion CSS and JavaScript

The amazing team at 10up created an accordion component package, which offers robust accessibility affordances. This is what we’ll rely on for hooking up the JavaScript. Some highlights include:

  • Extended key bindings to allow users to easily navigate in and out of accordion headings and content, e.g. home, end, up and down.
  • Relies on simple, native HTML elements to avoid unnecessary ARIA, e.g. button.

We can install this into our plugin with: npm install --save @10up/component-accordion

Goes in the src/accordion.js

/* * Accordion styling and state. */ import './accordion.scss'; import Accordion from '@10up/component-accordion'; if ( typeof window.ACFAccordionBlock !== 'object' ) { window.ACFAccordionBlock = {}; } window.ACFAccordionBlock.Accordion = Accordion; function accordionsInit() { new ACFAccordionBlock.Accordion( '.accordion' ); } document.addEventListener( 'DOMContentLoaded', () => { if ( window.acf ) { window.acf.addAction( 'render_block_preview', accordionsInit ); } else { accordionsInit(); } } );
Code language: JavaScript (javascript)

The complete and final CSS and JavaScript can be obtained from the final plugin’s repo:

  • src/accordion.scss
  • src/accordion.js

Build Our Final Block with @wordpress/scripts

Now that we have our plugin set up and activated and our accordion block ready to go, we need to run npm run build to build our block.

You should now be able to add and edit your Accordion block.

Accordion block as displayed in the Frost theme
The ACF Accordion block as displayed in the Frost theme (Dark mode)


There are quite a few steps to organize and build out a custom block. However, there are enhancements being made to the @wordpress/create-block package that should allow for a faster scaffolding experience.

I hope you found this tutorial helpful, and please reach out to me @dcook on Twitter if you hit any snags or have any questions.