Creating A Custom WordPress Block With ACF And register_block_type
Custom blocks give WordPress editors a structured way to add content without relying on a large collection of shortcodes, reusable HTML fragments, or page-builder controls. Advanced Custom Fields (ACF) makes this process accessible because fields can be defined in the WordPress admin while the block output remains under PHP and theme control.
The modern approach combines an ACF block definition with block.json and WordPress’s register_block_type() function. ACF supplies the editing interface and field data, while the Block Editor handles placement, previews, alignment, and saved content.
This pattern works well for cards, testimonials, call-to-action panels, staff profiles, property listings, event notices, and other components with a predictable layout. It is particularly useful for agencies managing several Australian websites, where editors in Sydney, Melbourne, Brisbane, or regional offices need consistent publishing tools.
A reliable implementation should account for editor usability, responsive CSS, caching, security, accessibility, and future maintenance. The code below uses a dynamic PHP-rendered block, which keeps content presentation controlled by the active theme or plugin rather than storing a large amount of generated markup in post content.
Choose The Block Architecture And Field Contract
Before writing PHP, decide what the block represents and which values an editor must provide. A testimonial block might require a quote, author name, role, image, and optional company URL. A promotional panel could use a heading, description, button label, button URL, background colour, and an image.
Keep the field model small and purposeful. A field should exist because it changes the component’s content or behaviour. Adding separate fields for every visual detail can make the editing screen slow to understand and encourage inconsistent designs. Colours, spacing, and typography are usually better controlled by CSS or block supports.
For example, a testimonial block could use these ACF fields:
quoteas a textarea or WYSIWYG fieldauthor_nameas a required text fieldauthor_roleas an optional text fieldauthor_imagereturning an image arrayauthor_urlreturning a URL
The block’s contract should also define what happens when a field is empty. If no image is supplied, the template should still produce a useful quote. If the URL is missing, the author name should remain plain text instead of producing an empty link. This makes the block resilient when editors update old content.
A dynamic block is generally preferable when the markup may change over time, when output depends on current site data, or when you want security filtering to happen each time the block renders. A static block can be suitable for content that must remain exactly as it was saved, but it requires more care when changing its JavaScript save function.
Register The ACF Block With WordPress
Create a directory inside a custom plugin or theme, such as blocks/testimonial. The directory can contain block.json, render.php, editor.css, and style.css. A plugin is often the better home for a reusable block because the component stays available when the site changes theme.
A basic block.json file can look like this:
{
"name": "acf/testimonial",
"title": "Testimonial",
"description": "Displays a customer testimonial.",
"category": "design",
"icon": "format-quote",
"keywords": ["quote", "review", "customer"],
"acf": {
"mode": "preview",
"renderTemplate": "render.php"
},
"supports": {
"anchor": true,
"align": ["wide", "full"],
"jsx": true
},
"style": "file:./style.css",
"editorStyle": "file:./editor.css"
}
The acf object tells ACF how to render the block. mode set to preview shows the rendered component in the editor, while edit initially displays the field form. The renderTemplate path is relative to block.json, so keeping both files in the same directory avoids confusing path calculations.
Register the block directory from the plugin’s main file:
<?php
/**
* Plugin Name: Site Blocks
*/
defined( 'ABSPATH' ) || exit;
add_action(
'init',
function () {
register_block_type( __DIR__ . '/blocks/testimonial' );
}
);
This is the modern registration method. WordPress reads the metadata from block.json, registers the block name, loads the CSS files, and connects the ACF rendering configuration. The register_block_type() call should run on init, after WordPress has loaded its block registry.
Older ACF tutorials may use acf_register_block_type() inside an acf/init callback. That approach still appears on established sites, but the metadata-based method is easier to maintain and aligns with current WordPress tooling. Avoid registering the same block through both functions, as duplicate registrations can create confusing editor behaviour.
Configure Fields And Render The Markup
Create an ACF field group and assign it to the block using the location rule for block type. Depending on the ACF version and interface, the rule will appear as a block condition such as Block is equal to Testimonial. Set the fields to match the names used by the PHP template.
The render.php file can retrieve the values with get_field() and escape them according to their context:
<?php
$quote = get_field( 'quote' );
$author_name = get_field( 'author_name' );
$author_role = get_field( 'author_role' );
$author_url = get_field( 'author_url' );
$image = get_field( 'author_image' );
$block_id = ! empty( $block['anchor'] )
? $block['anchor']
: 'testimonial-' . $block['id'];
$class_name = 'testimonial-block';
if ( ! empty( $block['className'] ) ) {
$class_name .= ' ' . $block['className'];
}
?>
<figure id="<?php echo esc_attr( $block_id ); ?>"
class="<?php echo esc_attr( $class_name ); ?>">
<?php if ( $quote ) : ?>
<blockquote class="testimonial-block__quote">
<?php echo wp_kses_post( $quote ); ?>
</blockquote>
<?php endif; ?>
<figcaption class="testimonial-block__caption">
<?php if ( $image && ! empty( $image['ID'] ) ) : ?>
<?php
echo wp_get_attachment_image(
$image['ID'],
'thumbnail',
false,
array(
'class' => 'testimonial-block__image',
'loading' => 'lazy',
)
);
?>
<?php endif; ?>
<span class="testimonial-block__author">
<?php if ( $author_url ) : ?>
<a href="<?php echo esc_url( $author_url ); ?>">
<?php echo esc_html( $author_name ); ?>
</a>
<?php else : ?>
<?php echo esc_html( $author_name ); ?>
<?php endif; ?>
<?php if ( $author_role ) : ?>
<span class="testimonial-block__role">
<?php echo esc_html( $author_role ); ?>
</span>
<?php endif; ?>
</span>
</figcaption>
</figure>
The escaping function should match the data type. Use esc_html() for plain text, esc_url() for links, esc_attr() for HTML attributes, and wp_kses_post() where an editor is allowed to enter limited formatting. Do not print a raw ACF value simply because it comes from an administrator account; capability changes, imports, and compromised accounts can alter the trust boundary.
An image field can return an ID, URL, or array. Returning an array is useful when the template needs responsive image data, alt text, or image dimensions. wp_get_attachment_image() generates a WordPress image element with registered sizes and helps the site serve more appropriate assets on mobile connections.
The HTML uses figure, blockquote, and figcaption because those elements describe the relationship between the quote and its attribution. Semantic markup supports screen readers and gives the component a meaningful structure without requiring extra ARIA attributes.
Add Editor Controls And Responsive Styling
The block’s field group controls its content, while block.json can expose standard editor features. The anchor support allows an editor to add a stable ID for internal links. Alignment support lets the block use the theme’s wide or full-width layout when appropriate.
Keep editor and front-end styling close enough that the preview represents the published component accurately. A simple stylesheet might include:
.testimonial-block {
margin: 2rem 0;
padding: clamp(1.25rem, 3vw, 2.5rem);
border: 1px solid #d8dde3;
border-radius: 0.5rem;
background: #fff;
}
.testimonial-block__quote {
margin: 0;
font-size: clamp(1.15rem, 2vw, 1.6rem);
line-height: 1.5;
}
.testimonial-block__caption {
display: flex;
gap: 0.75rem;
align-items: center;
margin-top: 1.25rem;
}
.testimonial-block__image {
width: 3rem;
height: 3rem;
border-radius: 50%;
object-fit: cover;
}
.testimonial-block__author,
.testimonial-block__role {
display: block;
}
.testimonial-block__role {
color: #5c6670;
font-size: 0.9rem;
}
Use clamp() and flexible spacing rather than assuming every visitor has a large desktop screen. A visitor on mobile broadband in regional New South Wales may have a very different loading experience from someone using a high-speed office connection in central Melbourne. Avoid unnecessary background images and request only the image size the component needs.
If the block has a link, make its focus state visible and ensure the colour contrast remains readable. Do not communicate meaning through colour alone. For example, a promotional badge should include text rather than relying only on a green or red background.
The editor stylesheet can adjust the preview canvas without changing front-end output:
.editor-styles-wrapper .testimonial-block {
max-width: 48rem;
margin-right: auto;
margin-left: auto;
}
When a block is registered from a plugin, its CSS is available regardless of the active theme. Theme-specific spacing should still be applied carefully, because a block may appear in a WooCommerce template, a full-width landing page, or a post layout with a narrow content column.
Handle Dynamic Values And WordPress Security
ACF blocks can use values from the current post, options pages, user records, taxonomies, or custom queries. If the component displays a related event or product, use WordPress APIs and escape each returned value. Avoid constructing SQL with field values. When a custom query is necessary, use WP_Query or $wpdb->prepare().
Dynamic output also needs clear permission boundaries. A field that stores a URL should accept only the format the block needs. For links, consider whether mailto: or other protocols should be allowed. esc_url() removes unsafe characters, but validation at input time provides an additional layer of control.
If the block includes a form, nonce handling and server-side validation are required. ACF field sanitisation does not automatically secure arbitrary form processing. For WooCommerce sites selling in Australia, payment and customer information should remain within established WooCommerce and payment-provider workflows rather than being copied into a custom block field.
Privacy should be considered when a block displays customer names, photos, testimonials, or location details. Australian organisations may need to account for the Privacy Act 1988 and the Australian Privacy Principles, particularly when publishing personal information or transferring it to external services. Obtain appropriate consent and provide a way to remove or update a testimonial when required.
Performance matters on sites serving visitors across Australia. Use local or properly configured content delivery infrastructure, generate responsive images, and avoid performing an expensive database query every time a block appears. Object caching or transient caching can help for public data, but cached content containing personal information requires careful expiry and invalidation rules.
Test The Block Before Deployment
A block should be tested in the editor, on the front end, and in the conditions where it will actually be used. Check empty fields, unusually long names, missing images, invalid URLs, multiple blocks on one page, and content pasted from external applications.
Use a staging copy before deploying changes to a live site. This is especially important for a busy online store in Sydney or Perth, where a PHP warning or malformed block.json file can affect checkout pages and editorial workflows. Test with the site’s real theme, caching layer, PHP version, and active plugins.
Editor and accessibility checks
- Insert the block several times and confirm each instance has unique markup.
- Navigate fields and links using only a keyboard.
- Test headings, focus states, contrast, and screen-reader order.
- Verify the block remains usable at narrow mobile widths.
Security and release checks
- Escape every field according to its output context.
- Confirm empty optional fields do not create broken HTML.
- Test with WordPress debugging enabled and PHP warnings visible.
- Clear page, object, and CDN caches after deployment.
Version the block files with the plugin or theme so changes can be reviewed and rolled back. If field names change, update the template and consider how existing posts will behave. Renaming an ACF field without a migration plan can make older blocks appear empty because their stored metadata still uses the previous key.
For a larger project, add automated PHP checks and a JavaScript build process only when they provide practical value. ACF blocks can be built with a modest amount of PHP, CSS, and metadata, so avoid introducing a complex toolchain for a component that does not need one. The important result is a stable editor experience, safe output, and a block that continues to render correctly as the WordPress site evolves.