Overview
Adding a new block to the plugin requires creating a directory under blocks/ with a few files. The plugin’s auto-discovery system handles registration automatically. No changes to any central configuration file are needed.
Step 1: Create the Block Directory
Create a new directory under blocks/. Use lowercase with hyphens:
blocks/my-custom-block/
Step 2: Create block.json
This file defines the block’s metadata for WordPress and ACF:
{
"apiVersion": 3,
"name": "acf/my-custom-block",
"title": "My Custom Block",
"description": "A brief description of what this block does.",
"category": "acf-blocks",
"icon": "admin-post",
"keywords": ["custom", "example"],
"acf": {
"renderTemplate": "my-custom-block.php",
"blockVersion": 3
},
"supports": {
"align": ["wide", "full"],
"mode": true,
"jsx": true,
"anchor": true
},
"style": "file:./my-custom-block.css"
}
Notes on the supports object:
align– Which alignment options to show (wide, full, left, right, center, ortruefor all).mode– Allows switching between edit and preview mode in the editor.jsx– Set totrueif your block uses InnerBlocks. Set tofalseor omit if it only uses ACF fields.anchor– Allows users to set a custom HTML anchor/ID on the block.
Step 3: Create block-data.json
This file defines the ACF field group. The location rule must reference your block name:
{
"key": "group_my_custom_block",
"title": "My Custom Block",
"fields": [
{
"key": "field_mcb_title",
"label": "Title",
"name": "mcb_title",
"type": "text",
"required": 1
},
{
"key": "field_mcb_content",
"label": "Content",
"name": "mcb_content",
"type": "wysiwyg",
"toolbar": "basic",
"media_upload": 0
},
{
"key": "field_mcb_style",
"label": "Style",
"name": "mcb_style",
"type": "select",
"choices": {
"default": "Default",
"dark": "Dark",
"accent": "Accent"
},
"default_value": "default"
}
],
"location": [
[
{
"param": "block",
"operator": "==",
"value": "acf/my-custom-block"
}
]
],
"active": true
}
Field group keys must be unique across all blocks. Use a consistent prefix derived from your block name.
Step 4: Create the Render Template
The render template is a PHP file that outputs the block’s HTML:
<?php
/**
* My Custom Block render template.
*
* @param array $block Block settings.
* @param string $content InnerBlocks content.
* @param bool $is_preview True in editor preview.
* @param int $post_id Current post ID.
*/
$title = acf_blocks_get_field( 'mcb_title', $block );
$content_html = acf_blocks_get_field( 'mcb_content', $block );
$style = acf_blocks_get_field( 'mcb_style', $block ) ?: 'default';
if ( empty( $title ) && empty( $content_html ) ) {
if ( $is_preview ) {
echo '<p style="padding: 20px; background: #f0f0f0;">My Custom Block — add content in the sidebar.</p>';
}
return;
}
$block_id = $block['id'] ?? '';
$class = 'acf-my-custom-block';
if ( ! empty( $block['className'] ) ) {
$class .= ' ' . $block['className'];
}
$class .= ' is-style-' . esc_attr( $style );
?>
<div id="<?php echo esc_attr( $block_id ); ?>" class="<?php echo esc_attr( $class ); ?>">
<?php if ( $title ) : ?>
<h3 class="acf-my-custom-block__title"><?php echo esc_html( $title ); ?></h3>
<?php endif; ?>
<?php if ( $content_html ) : ?>
<div class="acf-my-custom-block__content">
<?php echo wp_kses_post( $content_html ); ?>
</div>
<?php endif; ?>
</div>
Key practices:
- Always use
acf_blocks_get_field()instead ofget_field()for ACF 6.7+ compatibility. - Show a helpful placeholder when fields are empty and
$is_previewis true. - Use the
acf-prefix for all CSS classes. - Apply
esc_html()for text output andwp_kses_post()for HTML content.
Step 5: Create the Stylesheet (Optional)
.acf-my-custom-block {
padding: 24px;
border-radius: 8px;
margin-bottom: 24px;
}
.acf-my-custom-block.is-style-default {
background: #f9fafb;
border: 1px solid #e5e7eb;
}
.acf-my-custom-block.is-style-dark {
background: #1f2937;
color: #f9fafb;
}
.acf-my-custom-block.is-style-accent {
background: #eff6ff;
border-left: 4px solid #3b82f6;
}
.acf-my-custom-block__title {
margin: 0 0 12px;
font-size: 1.25em;
}
.acf-my-custom-block__content {
line-height: 1.6;
}
Step 6: Add extra.php (Optional)
If your block needs additional hooks, AJAX handlers, or scripts, create an extra.php file:
<?php
// Register a frontend script.
add_action( 'wp_enqueue_scripts', function() {
wp_register_script(
'acf-my-custom-block',
ACF_BLOCKS_PLUGIN_URL . 'blocks/my-custom-block/my-custom-block.js',
array(),
ACF_BLOCKS_VERSION,
true
);
});
Using Repeater Fields
For blocks with repeating content, define a repeater in your field group and use acf_blocks_get_repeater() in the template:
$items = acf_blocks_get_repeater( 'mcb_items', array(
'item_title',
'item_description',
'item_icon',
), $block );
foreach ( $items as $item ) {
echo '<div class="acf-my-custom-block__item">';
echo '<h4>' . esc_html( $item['item_title'] ) . '</h4>';
echo '<p>' . esc_html( $item['item_description'] ) . '</p>';
echo '</div>';
}
Using InnerBlocks
If your block supports InnerBlocks ("jsx": true), the $content variable contains the rendered inner content:
<div class="acf-my-custom-block__inner">
<InnerBlocks />
<?php echo $content; ?>
</div>
In the editor, <InnerBlocks /> is replaced by the InnerBlocks editing interface. On the frontend, $content contains the rendered HTML.
Verification
After creating your block files, reload any post editor. Your block should appear in the ACF Blocks category in the block inserter. If it does not appear:
- Confirm that
block.jsonis valid JSON. - Confirm that the
namefield starts withacf/. - Check
WP_DEBUG_LOGfor registration errors. - Verify that ACF Pro or Secure Custom Fields is active.