Why Custom Gutenberg Blocks Matter in Modern WordPress

WordPress powers over 43% of all websites on the internet. Since the introduction of the Gutenberg editor in WordPress 5.0 (December 2018), the way developers and content editors build pages has fundamentally changed. The block editor replaced the classic TinyMCE editor with a modular, block-based content system.

But here’s the reality: the default blocks — paragraphs, images, galleries, buttons — only get you so far. If you’re building a business website, an e-commerce platform, or a complex editorial site, you need custom blocks that match your exact design and functionality requirements.

That’s where Advanced Custom Fields (ACF) Pro enters the picture. It lets WordPress developers create custom Gutenberg blocks using PHP — the language most WordPress developers already know — instead of wrestling with React, JSX, and the complex @wordpress/scripts toolchain.

At Lueur Externe, a web agency based in the Alpes-Maritimes with over 20 years of experience in WordPress development, we’ve built hundreds of custom ACF blocks for clients across industries. This guide distills our real-world approach into actionable steps you can follow today.

Understanding the Architecture: How ACF Blocks Work

The Traditional (Hard) Way: Native Block Development

Building a native Gutenberg block typically involves:

  • Setting up a Node.js development environment
  • Writing JSX for the edit() and save() functions
  • Managing block attributes in JavaScript
  • Compiling with wp-scripts or a custom Webpack config
  • Handling serialization and validation

For a single block, you might write 200–400 lines of JavaScript before you even touch the front-end rendering.

The ACF Way: PHP-First Block Development

ACF Pro (version 5.8+, and significantly improved in ACF PRO 6.0+) introduced acf_register_block_type() — later updated to acf_register_block() — which lets you:

  1. Register a block in PHP
  2. Define its fields using the ACF field group UI (or PHP/JSON)
  3. Render it with a PHP template file
  4. Preview it live in the block editor

No JSX. No Webpack. No build step. Just PHP, HTML, and CSS.

Quick Comparison

FeatureNative Gutenberg BlockACF Pro Block
Primary languageJavaScript (React/JSX)PHP
Build step requiredYes (wp-scripts / Webpack)No
Editor previewCustom edit() functionPHP template (live preview)
Field managementBlock attributes (JS)ACF field groups (UI or PHP)
Learning curveSteepModerate
Ideal forComplex interactive blocksContent-driven, layout blocks
Time to build a block2–6 hours30 min – 2 hours

For 80–90% of client projects, ACF blocks are the faster, more maintainable choice.

Setting Up Your Development Environment

Prerequisites

Before you start, make sure you have:

  • WordPress 6.0+ (we recommend always using the latest stable release)
  • ACF PRO 6.0+ (the block features require the Pro version; free ACF does not support block registration)
  • A custom theme or a child theme (avoid adding blocks to a parent theme you don’t control)
  • A local development environment like LocalWP, DDEV, or Lando

Theme Structure for ACF Blocks

We recommend organizing your block files like this:

your-theme/
├── blocks/
│   ├── hero/
│   │   ├── block.json
│   │   ├── hero.php
│   │   ├── hero.css
│   │   └── hero.js (optional)
│   ├── testimonial/
│   │   ├── block.json
│   │   ├── testimonial.php
│   │   ├── testimonial.css
│   │   └── testimonial.js (optional)
│   └── cta-banner/
│       ├── block.json
│       ├── cta-banner.php
│       └── cta-banner.css
├── functions.php
└── style.css

Each block lives in its own directory with its own template, styles, and optional scripts. This keeps things modular and easy to maintain on large projects.

Registering Your First Custom ACF Block

Starting with ACF PRO 6.0 and WordPress 5.8+, the recommended way to register blocks is through a block.json metadata file. This aligns with WordPress core conventions and enables features like lazy loading of block assets.

Here’s a complete block.json for a Hero block:

{
  "name": "acf/hero",
  "title": "Hero Section",
  "description": "A customizable hero section with heading, text, image, and CTA button.",
  "category": "theme",
  "icon": "cover-image",
  "keywords": ["hero", "banner", "header"],
  "acf": {
    "mode": "preview",
    "renderTemplate": "blocks/hero/hero.php"
  },
  "styles": [
    { "name": "default", "label": "Default", "isDefault": true },
    { "name": "dark", "label": "Dark Background" }
  ],
  "supports": {
    "align": ["wide", "full"],
    "anchor": true,
    "jsx": true
  },
  "editorStyle": "file:./hero.css",
  "style": "file:./hero.css"
}

Then register the block in your functions.php:

add_action('init', function () {
    if (function_exists('acf_register_block_type')) {
        // ACF will read the block.json automatically
        register_block_type(get_template_directory() . '/blocks/hero');
    }
});

Method 2: Using acf_register_block_type() in PHP

If you prefer keeping everything in PHP (for example, on legacy projects), you can still use the PHP-only approach:

add_action('acf/init', function () {
    if (function_exists('acf_register_block_type')) {
        acf_register_block_type([
            'name'            => 'hero',
            'title'           => __('Hero Section'),
            'description'     => __('A customizable hero section.'),
            'render_template' => 'blocks/hero/hero.php',
            'category'        => 'theme',
            'icon'            => 'cover-image',
            'keywords'        => ['hero', 'banner'],
            'mode'            => 'preview',
            'supports'        => [
                'align'  => ['wide', 'full'],
                'anchor' => true,
            ],
            'enqueue_style'   => get_template_directory_uri() . '/blocks/hero/hero.css',
        ]);
    }
});

Both methods work. The block.json approach is more future-proof and benefits from WordPress’s built-in asset optimization.

Building the Block Template (PHP Rendering)

Here’s a real-world hero.php template:

<?php
/**
 * Hero Block Template
 *
 * @param array $block The block settings and attributes.
 * @param string $content The block inner HTML (empty for ACF blocks).
 * @param bool $is_preview True during editor preview.
 * @param int $post_id The post ID the block is on.
 * @param array $context The context provided to the block by the post or parent block.
 */

// Block ID and classes
$block_id = 'hero-' . ($block['id'] ?? uniqid());
$align_class = !empty($block['align']) ? ' align' . $block['align'] : '';
$style_class = !empty($block['className']) ? ' ' . $block['className'] : '';

// ACF fields
$heading    = get_field('heading') ?: 'Default Heading';
$subheading = get_field('subheading');
$cta_link   = get_field('cta_link'); // Link field (array)
$bg_image   = get_field('background_image'); // Image field (array)
?>

<section
    id="<?php echo esc_attr($block_id); ?>"
    class="block-hero<?php echo esc_attr($align_class . $style_class); ?>"
    <?php if ($bg_image): ?>
        style="background-image: url('<?php echo esc_url($bg_image['url']); ?>');" 
    <?php endif; ?>
>
    <div class="block-hero__content">
        <h2 class="block-hero__heading">
            <?php echo esc_html($heading); ?>
        </h2>

        <?php if ($subheading): ?>
            <p class="block-hero__subheading">
                <?php echo esc_html($subheading); ?>
            </p>
        <?php endif; ?>

        <?php if ($cta_link): ?>
            <a
                href="<?php echo esc_url($cta_link['url']); ?>"
                class="block-hero__cta"
                target="<?php echo esc_attr($cta_link['target'] ?: '_self'); ?>"
            >
                <?php echo esc_html($cta_link['title']); ?>
            </a>
        <?php endif; ?>
    </div>
</section>

Key Principles in This Template

  • Escape everything: esc_attr(), esc_html(), esc_url() — always.
  • Provide fallbacks: ?: 'Default Heading' prevents empty blocks in the editor.
  • Use the $block array: It contains alignment, class names, anchor, and block mode data.
  • Unique IDs: Essential when the same block is used multiple times on a page.

Leveraging InnerBlocks for Flexible Content

One of the most powerful features of ACF blocks is support for InnerBlocks — the ability to nest other Gutenberg blocks inside your custom block.

To enable InnerBlocks, add "jsx": true to your block’s supports in block.json (or 'jsx' => true in the PHP registration array). Then in your template:

<section class="block-content-section">
    <div class="block-content-section__inner">
        <InnerBlocks />
    </div>
</section>

You can even restrict which blocks are allowed inside:

<InnerBlocks
    allowedBlocks='["core/heading", "core/paragraph", "core/image", "core/list"]'
    template='[["core/heading",{"placeholder":"Section Title"}],["core/paragraph",{"placeholder":"Add your content here..."}]]'
/>

This gives content editors the freedom to compose rich layouts while staying within guardrails that protect the design system.

Defining ACF Field Groups for Your Blocks

You can create field groups through the ACF admin UI or define them in PHP for version control. Here’s a PHP example for our Hero block:

add_action('acf/init', function () {
    if (!function_exists('acf_add_local_field_group')) {
        return;
    }

    acf_add_local_field_group([
        'key'      => 'group_hero_block',
        'title'    => 'Hero Block Fields',
        'fields'   => [
            [
                'key'   => 'field_hero_heading',
                'label' => 'Heading',
                'name'  => 'heading',
                'type'  => 'text',
                'required' => 1,
            ],
            [
                'key'   => 'field_hero_subheading',
                'label' => 'Subheading',
                'name'  => 'subheading',
                'type'  => 'textarea',
                'rows'  => 2,
            ],
            [
                'key'   => 'field_hero_cta_link',
                'label' => 'CTA Button',
                'name'  => 'cta_link',
                'type'  => 'link',
            ],
            [
                'key'          => 'field_hero_bg_image',
                'label'        => 'Background Image',
                'name'         => 'background_image',
                'type'         => 'image',
                'return_format' => 'array',
                'preview_size' => 'medium',
            ],
        ],
        'location' => [
            [
                [
                    'param'    => 'block',
                    'operator' => '==',
                    'value'    => 'acf/hero',
                ],
            ],
        ],
    ]);
});

Defining field groups in PHP means they live in your Git repository. No more exporting JSON files or worrying about database-dependent configurations.

Performance Optimization for ACF Blocks

Custom blocks can become a performance bottleneck if you’re not careful. Here are the practices we follow at Lueur Externe on every production WordPress build:

Scope Your Assets

Don’t load block CSS and JS globally. Use the editorStyle, style, and script properties in block.json so WordPress only loads them when the block is actually used on the page.

Optimize Images in Blocks

When you output images from ACF fields, always use responsive sizes:

<?php if ($bg_image): ?>
    <img
        src="<?php echo esc_url($bg_image['sizes']['large']); ?>"
        srcset="<?php echo esc_attr(wp_get_attachment_image_srcset($bg_image['ID'])); ?>"
        sizes="(max-width: 768px) 100vw, 50vw"
        alt="<?php echo esc_attr($bg_image['alt']); ?>"
        loading="lazy"
        decoding="async"
        width="<?php echo esc_attr($bg_image['sizes']['large-width']); ?>"
        height="<?php echo esc_attr($bg_image['sizes']['large-height']); ?>"
    />
<?php endif; ?>

Minimize Database Queries

Each get_field() call is a database query. If you have a block with 10 fields, that’s 10 queries per block instance. On a page with 8 block instances, that’s 80 additional queries.

Solutions:

  • Use get_fields() (plural) to fetch all fields for a block in a single call
  • Implement object caching with Redis or Memcached
  • Use transients for blocks that display non-user-specific, rarely changing data
// Instead of multiple get_field() calls:
$fields = get_fields();
$heading = $fields['heading'] ?? 'Default Heading';
$subheading = $fields['subheading'] ?? '';
$cta_link = $fields['cta_link'] ?? null;
$bg_image = $fields['background_image'] ?? null;

Use Block Preloading

WordPress 6.0+ supports block-level asset optimization. When using block.json, styles and scripts are enqueued only when needed. This can reduce initial CSS payloads by 30–50% on pages that use only a subset of your custom blocks.

Real-World Block Library: What We Typically Build

After years of building WordPress sites with ACF blocks, we’ve found that most business websites need a core library of 10–15 custom blocks. Here’s what a typical set looks like:

  • Hero Section — full-width banner with heading, subtext, CTA, background image/video
  • Feature Grid — repeater-based grid of icons, titles, and descriptions
  • Testimonial Slider — client quotes with photo, name, company
  • CTA Banner — attention-grabbing call-to-action strip
  • Team Members — grid of profiles pulled from a CPT or repeater
  • FAQ Accordion — structured data-friendly Q&A section
  • Stats Counter — animated number counters (revenue, clients, projects)
  • Logo Carousel — partner/client logo showcase
  • Pricing Table — tiered pricing with feature comparison
  • Contact Form — integrated with Gravity Forms or WPForms
  • Content + Image Split — two-column layout with text on one side, image on the other
  • Video Embed — optimized lazy-loaded video with poster image

With ACF’s Repeater, Group, Flexible Content, and Clone field types, even complex blocks can be built in under two hours.

Common Pitfalls and How to Avoid Them

1. Not Testing in the Editor

Your block might look perfect on the front end but break in the Gutenberg editor. Always test both contexts. Use the $is_preview variable to conditionally adjust rendering:

<?php if ($is_preview && empty($heading)): ?>
    <div class="block-placeholder">
        <p>Please fill in the Hero block fields</p>
    </div>
<?php else: ?>
    <!-- Normal block output -->
<?php endif; ?>

2. Forgetting Allowed Block Types

If you use InnerBlocks without restrictions, editors can insert any block — including ones that break your layout. Always define allowedBlocks.

3. Ignoring Accessibility

  • Use semantic HTML (<section>, <nav>, <article>)
  • Add proper aria-labels where needed
  • Ensure sufficient color contrast in block styles
  • Make interactive elements keyboard-navigable

4. Hardcoding Content in Templates

Never hardcode text or URLs in your block PHP templates. Everything user-facing should come from an ACF field or a translatable string (__() / esc_html__()).

ACF Blocks vs. Other Approaches

You might be wondering how ACF blocks compare to other popular approaches:

  • Carbon Fields Blocks: Similar PHP-based approach, but with a smaller ecosystem and fewer field types than ACF Pro.
  • Genesis Custom Blocks (formerly Block Lab): Good for simple blocks, but lacks ACF’s field depth (no Repeater, Flexible Content, etc.).
  • Meta Box Blocks: Comparable to ACF in power, with a slightly different API. Meta Box’s block rendering also uses PHP templates.
  • Full native development: Maximum flexibility but significantly higher development time and maintenance cost.

ACF Pro remains the most popular choice for PHP-centric WordPress block development, with over 2 million active installations and a mature, well-documented API.

Workflow Tips for Teams

Version-Control Your Fields

Use ACF’s Local JSON feature (or PHP-defined field groups) so your field configurations are stored in your theme’s acf-json/ directory and tracked in Git. This eliminates the “it works on my machine” problem.

Use a Block Starter Template

Create a boilerplate block directory with placeholder block.json, .php, and .css files. When a new block is needed, duplicate the starter and customize. This consistency saves time and reduces errors across a team.

Document Your Blocks

Maintain a simple internal doc (even a README in your theme) listing each block, its fields, supported alignments, and any special behavior. Future you — or the next developer — will be grateful.

Conclusion: Build Better WordPress Sites with ACF Blocks

Custom Gutenberg blocks built with ACF Pro represent the sweet spot of modern WordPress development: powerful enough for complex layouts, accessible enough for PHP developers, and editor-friendly enough for non-technical content teams.

By following the practices outlined in this guide — modular file structure, block.json registration, scoped assets, responsive images, and proper escaping — you’ll build blocks that are fast, maintainable, and a joy to use.

At Lueur Externe, building performant, custom WordPress solutions with ACF and Gutenberg is part of our daily work. Whether you need a single custom block or a full block-based theme architecture, our team of certified WordPress specialists in the Alpes-Maritimes is ready to help.

Ready to elevate your WordPress project? Get in touch with Lueur Externe and let’s build something exceptional together.