Creating Custom Blocks

  • JNext lesson
  • KPrevious lesson
  • FSearch lessons
  • EscClear search

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, or true for all).
  • mode – Allows switching between edit and preview mode in the editor.
  • jsx – Set to true if your block uses InnerBlocks. Set to false or 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 of get_field() for ACF 6.7+ compatibility.
  • Show a helpful placeholder when fields are empty and $is_preview is true.
  • Use the acf- prefix for all CSS classes.
  • Apply esc_html() for text output and wp_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:

  1. Confirm that block.json is valid JSON.
  2. Confirm that the name field starts with acf/.
  3. Check WP_DEBUG_LOG for registration errors.
  4. Verify that ACF Pro or Secure Custom Fields is active.