Build Custom Gutenberg Blocks Using ACF Blocks (No React Required)

·

Build Custom Gutenberg Blocks with ACF Blocks in WordPress

For years, building custom Gutenberg blocks meant learning React, setting up a build pipeline, and wrestling with JavaScript tooling most PHP developers never signed up for. ACF Blocks changes that. If you already know PHP and use Advanced Custom Fields on your projects, you can build production-ready custom Gutenberg blocks today – with the same familiar get_field() calls, no React, no npm, no build step.

This guide walks through building a real plugin with three custom Gutenberg blocks using ACF Blocks, progressing from simple to advanced.

What You’ll Build

Three production-ready blocks packaged as a single plugin (wpvibes-acf-blocks):

  • Testimonial Block: a customer quote with author name, title, and photo
  • Feature CTA Block: a feature card with image, heading, description, and button link
  • Team Members Block: a responsive grid of team members using ACF’s Repeater field, with configurable column count

Each block lives in its own folder inside the plugin. Adding a new block later means creating one new folder – no changes to any existing file.

Prerequisites

  • WordPress 6.7 or later (any version supporting ACF Blocks V3)
  • ACF Pro: required for building custom blocks. The free version of ACF does not support this. ACF Pro is $49/year for a single site.
  • Local development environment (LocalWP, WordPress Studio, wp-env, or similar)
  • Any WordPress theme: block themes (Twenty Twenty-Four) or classic themes both work. No child theme required since we’re using a plugin.
  • Basic PHP knowledge: you’ll write PHP templates using standard ACF functions

Step 1: Set Up the Plugin

The plugin contains all three blocks in one folder structure, organized so adding new blocks later is trivial.

1.1 – File structure

Create this folder inside wp-content/plugins/:

wpvibes-acf-blocks/
├── wpvibes-acf-blocks.php
└── blocks/
├── testimonial/
│ ├── block.json
│ ├── render.php
│ └── style.css
├── feature-cta/
│ ├── block.json
│ ├── render.php
│ └── style.css
└── team-members/
├── block.json
├── render.php
└── style.css

Why this structure?

  • One block = one folder – every block owns its block.jsonrender.php, and style.css in the same place. Delete the folder and the block is gone.
  • Auto-registration – the main plugin file scans blocks/ and registers every folder it finds. Adding a new block never requires touching this file.
  • Per-block CSS – each block’s styles are in its own file, loaded automatically when the block appears on a page.

This is the same pattern used in our PHP-only blocks guide – one block, one folder, self-contained.

1.2 – The main plugin file

Create wpvibes-acf-blocks.php:

<?php
/**
 * Plugin Name: WPVibes ACF Blocks
 * Description: Custom Gutenberg blocks built with Advanced Custom Fields — Testimonial, Feature CTA, and Team Members.
 * Version:     1.0.0
 * Author:      WPVibes
 * Requires at least: 6.7
 * Requires PHP: 7.4
 * Text Domain: wpvibes-acf-blocks
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Register all ACF blocks by scanning the blocks/ folder.
 * Adding a new block later = one new folder, zero changes here.
 */
add_action( 'init', 'wpvibes_acf_register_blocks' );

function wpvibes_acf_register_blocks() {

	// Only run if ACF Pro is active.
	if ( ! function_exists( 'acf_register_block_type' ) ) {
		return;
	}

	$blocks_dir = plugin_dir_path( __FILE__ ) . 'blocks/';

	if ( ! is_dir( $blocks_dir ) ) {
		return;
	}

	// Register every block folder inside blocks/.
	foreach ( glob( $blocks_dir . '*', GLOB_ONLYDIR ) as $block_folder ) {
		register_block_type( $block_folder );
	}
}

What this does in plain words:

  • The plugin header identifies this folder as a WordPress plugin
  • wpvibes_acf_register_blocks() runs on WordPress’s init hook
  • ACF Pro check first — if ACF Pro isn’t active, the function exits silently (no fatal errors)
  • glob() finds every folder inside blocks/
  • register_block_type( $block_folder ) tells WordPress: “there’s a block here, read block.json for details”

Once this loop exists, you never touch this file again. Every new block you add as a folder inside blocks/ registers automatically.

Activate the plugin from Plugins → Installed Plugins. Nothing visible yet – we haven’t built any blocks.

Step 2: Build the Testimonial Block

The Testimonial block is the simplest of the three. It shows a customer quote with author details and photo. This block introduces the complete ACF Blocks workflow – block.json, field group setup, PHP template, and CSS.

Testimonial block rendered on the frontend
Testimonial block rendered on the frontend

2.1 – Create block.json

Create blocks/testimonial/block.json:

{
	"$schema": "https://schemas.wp.org/trunk/block.json",
	"apiVersion": 3,
	"name": "wpvibes/testimonial",
	"title": "Testimonial",
	"description": "Display a customer testimonial with author details and photo.",
	"category": "text",
	"icon": "format-quote",
	"keywords": ["testimonial", "quote", "review", "author"],
	"supports": {
		"align": ["wide", "full"],
		"anchor": true,
		"jsx": true
	},
	"acf": {
		"mode": "preview",
		"renderTemplate": "render.php"
	},
	"style": "file:./style.css"
}

Line by line, in plain words:

  • apiVersion: 3 – required for ACF Blocks V3. Always use v3 for new blocks.
  • name: "wpvibes/testimonial" – the block’s unique identifier. Format: {namespace}/{block-slug}.
  • titledescriptioncategoryiconkeywords – how the block appears in the block inserter.
  • supports.align – allows wide and full-width alignment.
  • supports.anchor – lets users add an HTML anchor (for jump-to links).
  • supports.jsx: true – required for ACF Blocks V3.
  • acf.mode: "preview" – show the rendered block in the editor (not a form view).
  • acf.renderTemplate – the PHP file that renders the block. Relative to block.json.
  • style – the stylesheet, loaded automatically in both the editor and frontend.

2.2 – Create the ACF Field Group

The Testimonial block needs four fields. Create them through the ACF admin UI.

  • Go to wp-admin → ACF → Field Groups → Add New
  • Field group title: Testimonial Block Fields
  • Add the following four fields:
Field LabelField NameField TypeNotes
Testimonial Texttestimonial_textTextareaRequired, Character Limit: 500
Author Nameauthor_nameTextRequired
Author Titleauthor_titleTextOptional, e.g., “CEO, Acme Corp”
Author Imageauthor_imageImageReturn Format: Image Array, Preview Size: Medium
Create ACF Field Group for Testimonial Block
Create ACF Field Group for Testimonial Block
  • Scroll down to Settings → Location Rules
  • Set the rule: Show this field group if → Block → is equal to → Testimonial
  • Click Publish
Set ACF Field Group Settings
Set ACF Field Group Settings

2.3 – Create render.php

Create blocks/testimonial/render.php:

<?php
/**
 * Testimonial block render template.
 *
 * @var array  $block      The block settings and attributes.
 * @var string $content    The block inner HTML (empty for ACF Blocks).
 * @var bool   $is_preview True during backend preview render.
 */

// Get field values.
$testimonial_text = get_field( 'testimonial_text' );
$author_name      = get_field( 'author_name' );
$author_title     = get_field( 'author_title' );
$author_image     = get_field( 'author_image' );

// Build CSS classes — always include the base class + any WordPress-added classes.
$class_name = 'wpvibes-testimonial';
if ( ! empty( $block['className'] ) ) {
	$class_name .= ' ' . $block['className'];
}
if ( ! empty( $block['align'] ) ) {
	$class_name .= ' align' . $block['align'];
}

// Support the anchor attribute.
$anchor = ! empty( $block['anchor'] ) ? 'id="' . esc_attr( $block['anchor'] ) . '" ' : '';
?>

<div <?php echo $anchor; ?>class="<?php echo esc_attr( $class_name ); ?>">

	<?php if ( $testimonial_text ) : ?>
		<blockquote class="wpvibes-testimonial__quote">
			<?php echo esc_html( $testimonial_text ); ?>
		</blockquote>
	<?php endif; ?>

	<div class="wpvibes-testimonial__author">

		<?php if ( $author_image ) : ?>
			<img
				class="wpvibes-testimonial__image"
				src="<?php echo esc_url( $author_image['sizes']['thumbnail'] ); ?>"
				alt="<?php echo esc_attr( $author_image['alt'] ); ?>"
				width="60"
				height="60"
			/>
		<?php endif; ?>

		<div class="wpvibes-testimonial__info">
			<?php if ( $author_name ) : ?>
				<p class="wpvibes-testimonial__name"><?php echo esc_html( $author_name ); ?></p>
			<?php endif; ?>

			<?php if ( $author_title ) : ?>
				<p class="wpvibes-testimonial__title"><?php echo esc_html( $author_title ); ?></p>
			<?php endif; ?>
		</div>

	</div>

</div>

Key points:

  • get_field() – ACF’s standard function for retrieving field values. Works exactly the same in ACF Blocks as it does anywhere else in ACF.
  • $block['className'] – WordPress passes any classes the user added via the sidebar’s Advanced section.
  • $block['align'] – the alignment the user picked (wide, full).
  • $block['anchor'] – the HTML ID the user set.
  • Every field wrapped in if ( $field ) – optional fields don’t render empty tags.
  • esc_html() and esc_url() – always escape output. Standard WordPress security.

The $author_image array structure (when Return Format is “Image Array”):

  • $author_image['url'] – full-size URL
  • $author_image['sizes']['thumbnail'] – thumbnail URL
  • $author_image['sizes']['medium'] – medium URL
  • $author_image['alt'] – alt text

2.4 – Create style.css

Create blocks/testimonial/style.css:

.wpvibes-testimonial {
	background: #f9fafb;
	padding: 32px;
	border-radius: 12px;
	max-width: 640px;
	margin: 1.5em auto;
}

.wpvibes-testimonial__quote {
	font-size: 18px;
	line-height: 1.6;
	color: #1f2937;
	font-style: italic;
	margin: 0 0 24px;
	border: none;
}

.wpvibes-testimonial__author {
	display: flex;
	align-items: center;
	gap: 12px;
}

.wpvibes-testimonial__image {
	width: 60px;
	height: 60px;
	border-radius: 50%;
	object-fit: cover;
}

.wpvibes-testimonial__name {
	margin: 0;
	font-weight: 600;
	color: #111827;
}

.wpvibes-testimonial__title {
	margin: 0;
	font-size: 14px;
	color: #6b7280;
}

2.5 – See it working

  1. Create a new page: Pages → Add New
  2. Click + in the block inserter and search for Testimonial
  3. Insert the block
  4. Fill in the fields on the right sidebar:
    • Testimonial Text: "WPVibes plugins saved us weeks of development time. The support is fantastic."
    • Author Name: Sarah Chen
    • Author Title: Lead Developer, TechCorp
    • Author Image: upload a headshot
  5. Watch the preview update in real time
  6. Preview the frontend
Testimonial field group with all 4 fields and location rule visible
Testimonial field group with all 4 fields and location rule visible

Your first ACF block is done – no React, no build step, just PHP and CSS you already know.

Step 3: Build the Feature CTA Block

The Feature CTA block introduces two new patterns – the Link field (URL + label + target in one field) and using images at different sizes. Same block structure as Testimonial, just different fields.

Feature CTA block rendered on the frontend
Feature CTA block rendered on the frontend

3.1 – Create block.json

Create blocks/feature-cta/block.json:

{
	"$schema": "https://schemas.wp.org/trunk/block.json",
	"apiVersion": 3,
	"name": "wpvibes/feature-cta",
	"title": "Feature CTA",
	"description": "A feature card with image, heading, description, and call-to-action button.",
	"category": "design",
	"icon": "megaphone",
	"keywords": ["feature", "cta", "call to action", "card"],
	"supports": {
		"align": ["wide", "full"],
		"anchor": true,
		"jsx": true
	},
	"acf": {
		"mode": "preview",
		"renderTemplate": "render.php"
	},
	"style": "file:./style.css"
}

3.2 – Create the ACF Field Group

Create a new field group called Feature CTA Block Fields with these four fields:

Field LabelField NameField TypeNotes
Feature Imagefeature_imageImageReturn Format: Image Array
HeadingheadingTextRequired
DescriptiondescriptionTextareaRows: 3
Button Linkbutton_linkLinkReturn Format: Link Array

Set the location rule: Show this field group if → Block → is equal to → Feature CTA.

3.3 – Create render.php

Create blocks/feature-cta/render.php:

<?php
/**
 * Feature CTA block render template.
 *
 * @var array $block The block settings and attributes.
 */

$feature_image = get_field( 'feature_image' );
$heading       = get_field( 'heading' );
$description   = get_field( 'description' );
$button_link   = get_field( 'button_link' );

$class_name = 'wpvibes-feature-cta';
if ( ! empty( $block['className'] ) ) {
	$class_name .= ' ' . $block['className'];
}
if ( ! empty( $block['align'] ) ) {
	$class_name .= ' align' . $block['align'];
}

The full render template, with the image and heading markup included, is here: blocks/feature-cta/render.php.

What’s new here compared to Testimonial:

  • The Link field returns an array with three keys: urltitletarget. Handle each carefully with the appropriate escape function.
  • rel="noopener noreferrer" – security best practice when opening links in a new tab. Without this, the new tab can access the original window through window.opener.
  • Fallback for target – if the user didn’t set a target, default to _self.

3.4 – Create style.css

Create blocks/feature-cta/style.css:

.wpvibes-feature-cta {
	display: flex;
	flex-direction: column;
	align-items: center;
	text-align: center;
	gap: 20px;
	padding: 40px 32px;
	background: #ffffff;
	border-radius: 12px;
	box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
	max-width: 400px;
	margin: 1.5em auto;
}

.wpvibes-feature-cta__image {
	width: 80px;
	height: 80px;
}

.wpvibes-feature-cta__image img {
	max-width: 100%;
	max-height: 100%;
	object-fit: contain;
}

3.5 – See it working

Insert Feature CTA from the Design category. Upload a small icon-style image, add a heading like “Fast Deployment,” a description, and a button label with a URL. Preview the frontend.

Feature CTA block filled in the editor
Feature CTA block filled in the editor

Same pattern as Testimonial, more fields, one new field type. The plugin pattern is starting to feel automatic.

Step 4: Build the Team Members Block (with Repeater)

The Team Members block is where ACF Blocks really shines. Using ACF’s Repeater field, users add unlimited team members through the sidebar – no coding required. This is the pattern that makes ACF Pro worth the license fee.

The block also introduces a column selector so users pick between 2, 3, or 4 columns.

Team Members grid rendered on the frontend
Team Members grid rendered on the frontend

4.1 – Create block.json

Create blocks/team-members/block.json:

{
	"$schema": "https://schemas.wp.org/trunk/block.json",
	"apiVersion": 3,
	"name": "wpvibes/team-members",
	"title": "Team Members",
	"description": "Display a grid of team members using ACF Repeater fields.",
	"category": "design",
	"icon": "groups",
	"keywords": ["team", "members", "people", "staff"],
	"supports": {
		"align": ["wide", "full"],
		"anchor": true,
		"jsx": true
	},
	"acf": {
		"mode": "preview",
		"renderTemplate": "render.php"
	},
	"style": "file:./style.css"
}

4.2 – Create the ACF Field Group

Create a field group called Team Members Block Fields with these fields:

Block-level fields (things that apply to the whole grid):

Field LabelField NameField TypeNotes
Section Headingsection_headingTextOptional
ColumnscolumnsTextEnter the number of columns you want
Team Membersteam_membersRepeaterLayout: Block. Button Label: “Add Team Member”

Sub-fields inside the team_members repeater:

Sub-field LabelSub-field NameSub-field TypeNotes
Photomember_imageImageReturn Format: Image Array, Preview Size: Medium
Namemember_nameTextRequired
Rolemember_roleTextOptional
Biomember_bioTextareaRows: 3, Optional

Set the location rule: Show this field group if → Block → is equal to → Team Members.

Team Members field group with the repeater and sub-fields visible
Team Members field group with the repeater and sub-fields visible

4.3 – Create render.php

Create blocks/team-members/render.php:

<?php
/**
 * Team Members block render template.
 *
 * @var array $block The block settings and attributes.
 */

// Block-level fields.
$section_heading = get_field( 'section_heading' );
$columns         = get_field( 'columns' );
$columns         = $columns ? $columns : 3;

// Build CSS classes.
$class_name = 'wpvibes-team wpvibes-team--cols-' . intval( $columns );
if ( ! empty( $block['className'] ) ) {
	$class_name .= ' ' . $block['className'];
}
if ( ! empty( $block['align'] ) ) {
	$class_name .= ' align' . $block['align'];
}

$anchor = ! empty( $block['anchor'] ) ? 'id="' . esc_attr( $block['anchor'] ) . '" ' : '';
?>

The full render template, with every sub-field’s markup included, is here: blocks/team-members/render.php.

Three ACF Pro functions doing the heavy lifting:

  • have_rows( 'team_members' ) – checks if the repeater has any rows. Similar in shape to WordPress’s have_posts().
  • the_row() – loads the current row’s data so get_sub_field() calls work.
  • get_sub_field( 'field_name' ) – retrieves a sub-field value from the current row.

Once you understand this three-function loop, you can build any repeater-based block – image galleries, feature lists, pricing tiers, timelines, FAQ accordions. The pattern is always the same.

4.4 – Create style.css

Create blocks/team-members/style.css:

.wpvibes-team {
	margin: 2em auto;
}

.wpvibes-team__grid {
	display: grid;
	gap: 32px;
}

.wpvibes-team--cols-2 .wpvibes-team__grid { grid-template-columns: repeat(2, 1fr); }
.wpvibes-team--cols-3 .wpvibes-team__grid { grid-template-columns: repeat(3, 1fr); }
.wpvibes-team--cols-4 .wpvibes-team__grid { grid-template-columns: repeat(4, 1fr); }

.wpvibes-team__member {
	display: flex;
	flex-direction: column;
	align-items: center;
	text-align: center;
	padding: 32px 24px;
	background: #ffffff;
	border-radius: 12px;
	box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}

4.5 – See it working

  1. Insert Team Members from the Design category
  2. Set section heading: “Meet Our Team”
  3. Set columns to 3
  4. Click Add Team Member – add several members with photo, name, role, and bio
  5. Watch the grid render in the editor preview
  6. Try changing columns to 2 or 4 to see the layout adapt
Team Members block in the editor with 4 members and column selector visible
Team Members block in the editor with 4 members and column selector visible

Three real, production-ready blocks in one plugin – done.

Get the Full Plugin

The complete plugin – with all three blocks, full CSS, and README – is on GitHub:

To install:

cd wp-content/plugins/
git clone https://github.com/webtechhardik/wpvibes-acf-blocks.git

Then activate from Plugins → Installed Plugins. Adding a new block:

  1. Create a new folder inside blocks/ (e.g., blocks/pricing-table/)
  2. Add block.jsonrender.php, and style.css to it
  3. Create the matching ACF field group with location rule Block = Pricing Table

That’s it. No changes to wpvibes-acf-blocks.php, no manual registration – the loader handles everything.

Why ACF Blocks Makes Block Development Easy

Building custom Gutenberg blocks the “official” way means learning React, setting up a JavaScript build pipeline (Webpack, Babel, Node.js), and mastering the block editor’s JavaScript APIs. That’s a real learning curve – several weeks of investment before shipping the first block.

ACF Blocks removes almost all of that. If you already know PHP and use ACF on your projects, the entire mental shift is small:

  • You write PHP templates – the same syntax you’ve been writing for years
  • You use get_field() – the same function you already use everywhere else
  • ACF handles the editor experience – field UI, live preview, validation, all automatic
  • No build step – save PHP, refresh the browser

The tradeoff is that ACF Pro costs $49/year for a single site. For agencies and freelancers building client sites, that’s a rounding error compared to the time saved on every project.

For blocks that need advanced editor experiences (drag-and-drop, deep interactivity, custom media handling), React blocks still make sense. But for the vast majority of custom blocks a WordPress site actually needs – testimonials, feature cards, team grids, pricing tables, hero sections – ACF Blocks does the job faster with less friction.

Frequently Asked Questions

Do I need ACF Pro, or does the free version work?

ACF Pro is required. Custom block registration is a Pro-only feature. The free ACF plugin supports custom fields on posts and pages, but not custom Gutenberg blocks. ACF Pro is $49/year for a single site.

Can I use ACF Blocks in a child theme instead of a plugin?

Yes. The official ACF tutorial uses this approach. The plugin approach shown in this guide is better for portability – the blocks work regardless of which theme is active.

Why does get_field() return null in my ACF Block?

The most common cause is the field group’s location rule. It must be set exactly to Block = [Block Name]. If the rule says “Post Type” or anything else, the fields don’t attach to the block. This trips up nearly every developer at least once.

Can ACF Blocks use InnerBlocks (nested blocks)?

Yes. Add <InnerBlocks /> to your render.php template. ACF officially supports this in ACF Blocks V3. This gives you container blocks where users can nest core blocks inside.

How do I add a custom icon instead of a Dashicon?

Two ways. Use any Dashicon slug in block.json‘s icon field (like format-quote or groups). Or, with WordPress 7.1+, register your own SVG icon and reference it. See our Custom SVG Icons guide for the full process.

What’s the difference between ACF Blocks and PHP-only blocks (WordPress 7.0+)?

ACF Blocks require ACF Pro but support 30+ field types (repeater, image, gallery, relationship, etc.). WordPress 7.0’s PHP-only blocks are free but limited to 4 attribute types (string, integer, boolean, enum). For simple blocks, PHP-only works. For anything with images, repeaters, or complex data, ACF Blocks wins.

How do I loop through a repeater on the frontend?

Use the standard ACF repeater loop: have_rows()the_row(), and get_sub_field(). This pattern works exactly the same in an ACF Block as it does anywhere else in ACF. The Team Members block in this guide is a full working example.

Can I ship an ACF field group in the plugin so users don’t have to create it manually?

Yes, using ACF’s JSON sync feature. Create an acf-json/ folder in the plugin root. When users save a field group, ACF writes it to that folder. Include those JSON files in the plugin zip. When a user activates the plugin, ACF loads the field group automatically. This is the approach ACF’s official documentation recommends for shipping fields with plugins or themes.

Wrapping Up

Custom Gutenberg blocks don’t require learning React. If you know PHP and use ACF on your projects, you can start shipping real blocks today – with the same familiar workflow you already use for everything else.

The takeaways:

  1. The pattern is consistent – every ACF block is block.json + field group + render.php + style.css in one folder
  2. The location rule is critical – always set the field group to Block = [Block Name], or get_field() returns null
  3. One block = one folder – self-contained, easy to add, easy to remove
  4. Auto-registration scales – the plugin scans blocks/ and registers every folder, so adding a new block requires zero changes to existing files
  5. The Repeater field is the payoff – three functions (have_rowsthe_rowget_sub_field) unlock unlimited grid layouts, team lists, pricing tiers, and more

Leave a Reply

Your email address will not be published. Required fields are marked *

Featured Plugin

Frontend Product Editor

Edit WooCommerce products directly from the frontend — no backend switching needed. Save your team hours every week.

  • Edit pricing, SKU & inventory
  • Supports all product types
  • Role-based access control

You Might Also Like

Enjoyed this article?

Get the latest WordPress tips, tutorials and product news, straight to your inbox.