Developed By Ken Key

Advanced Custom Fields in WordPress: A Practical Guide for 2026

By Ken Key Apr 15, 2026 6 min read Updated Oct 16, 2026

Advanced Custom Fields in WordPress: A Practical Guide for 2026
The short answer

ACF in 2026 means ACF PRO, since repeaters, flexible content, and options pages live there. I register field groups in PHP or local JSON so they version with the theme in git, and I use ACF Blocks for custom Gutenberg blocks without a React build step.

Part of the complete web development guide.

On this page

Advanced Custom Fields has been the default answer to “where do I put this content?” in WordPress for over a decade. But the plugin changed a lot in the last few years, and a lot of the tutorials still floating around are showing patterns that are either deprecated or just plain slower than they need to be. Here’s how I actually use ACF in 2026, including where it stops making sense.

What ACF looks like in 2026

A few things have shifted since the old days of clicking through the field group UI:

  • ACF PRO is the only version with the good stuff. Repeaters, flexible content, clone fields, and the options page are all PRO. The free version gives you basic fields, and that’s it.
  • Block registration is the headline feature. ACF Blocks let you register a custom Gutenberg block with a block.json file and a render template, no React build step required. For most client sites this is the fastest path to custom blocks.
  • Local JSON is still the move. Field groups saved to /acf-json/ get version-controlled with your theme and load without database queries on every request.
  • ACF 6.x requires PHP 7.4+. If you’re still on PHP 7.2, upgrade. It’s 2026.

Registering fields in code, not the admin

For anything I’m going to ship to a client, I register field groups in PHP or JSON, not by clicking around the admin. Reason: the admin is a database. The database doesn’t go in git. When you push to staging, your fields don’t come with you.

Here’s a minimal example. This registers a field group on the page post type with a heading and a repeater of call-to-action buttons:

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

 acf_add_local_field_group( [
 'key' => 'group_hero_section',
 'title' => 'Hero Section',
 'fields' => [
 [
 'key' => 'field_hero_heading',
 'label' => 'Heading',
 'name' => 'hero_heading',
 'type' => 'text',
 ],
 [
 'key' => 'field_hero_ctas',
 'label' => 'Buttons',
 'name' => 'hero_ctas',
 'type' => 'repeater',
 'min' => 0,
 'max' => 3,
 'layout' => 'block',
 'sub_fields' => [
 [
 'key' => 'field_cta_label',
 'label' => 'Label',
 'name' => 'label',
 'type' => 'text',
 ],
 [
 'key' => 'field_cta_url',
 'label' => 'URL',
 'name' => 'url',
 'type' => 'url',
 ],
 ],
 ],
 ],
 'location' => [
 [
 [
 'param' => 'post_type',
 'operator' => '==',
 'value' => 'page',
 ],
 ],
 ],
 ] );
} );

One gotcha: the key values have to be unique and stable. If you change a key later, ACF treats it as a new field and the existing content is orphaned. Pick keys once and don’t touch them.

Rendering the values

The get_field() vs the_field() distinction still trips people up. Use get_field() when you need to do something with the value (escape it, loop it, pass it to a function). Use the_field() only when you want it echoed raw, and honestly, just don’t. Escape everything.

$heading = get_field( 'hero_heading' );
$ctas = get_field( 'hero_ctas' );

if ( $heading ) {
 printf( '<h1>%s</h1>', esc_html( $heading ) );
}

if ( $ctas ) {
 echo '<div class="hero-ctas">';
 foreach ( $ctas as $cta ) {
 printf(
 '<a class="btn" href="%s">%s</a>',
 esc_url( $cta['url'] ),
 esc_html( $cta['label'] )
 );
 }
 echo '</div>';
}

Notice esc_url() on the URL and esc_html() on the label. ACF does not sanitize output for you. If a client can type into a field, that field is user input, and it needs escaping on the way out.

ACF Blocks: the practical sweet spot

If you’re building custom Gutenberg blocks, ACF Blocks are usually the shortest path. You get the field UI, the block editor integration, and a PHP template, without a webpack build.

Start with a block.json in your theme or plugin:

{
 "name": "acf/testimonial",
 "title": "Testimonial",
 "category": "formatting",
 "icon": "format-quote",
 "acf": {
 "mode": "preview",
 "renderTemplate": "blocks/testimonial/render.php"
 },
 "supports": {
 "align": false,
 "jsx": false
 }
}

Then register it from PHP:

add_action( 'init', function() {
 register_block_type( get_template_directory() . '/blocks/testimonial' );
} );

And your render.php just uses the same get_field() API. The block attributes are available via $block if you need them, but for a straight content block, the ACF fields are usually enough.

The one thing ACF Blocks don’t do well: complex interactive client-side components. If your block needs React state, a data store, or a custom inspector control with live preview, you’re better off with the native block API. ACF Blocks are for content, not apps.

Performance notes that actually matter

ACF is not slow. Bad usage of ACF is slow. The usual culprits:

  • N+1 queries on repeaters inside loops. If you’re looping posts and calling get_field() on each one, and those fields are stored as post meta, WP is firing a query per post. Use update_post_meta_cache() or pull the meta in your main query.
  • Loading field groups from the database on the front end. Local JSON fixes this. If your fields are only in the admin, ACF has to hit the DB to know what fields exist, even on pages that don’t render any.
  • Options pages with huge repeaters. An options page with a 200-row repeater gets serialized into a single option row. Every page load deserializes the whole thing. If you need that much data, it’s a custom post type, not an options page.

When I skip ACF entirely

ACF is the right tool for content that a non-technical editor needs to manage. It’s the wrong tool for:

  • Structured data that belongs in a custom post type. If you’re storing 50 rows of “team members” as a repeater on one page, that’s a CPT with a taxonomy, and you get search, archives, and pagination for free.
  • Anything that needs to be queried. ACF fields are post meta. Post meta queries are fine for filtering a handful of things, but if you’re building a directory or a filterable listing, design the data model first.
  • Global settings that rarely change. Sometimes a theme.json value or a hardcoded constant is cleaner than an options page nobody remembers to update.

I’ve built plenty of sites where ACF is doing 80% of the heavy lifting, and plenty where it’s not installed at all. The deciding question is always the same: does a human need to edit this through the admin? If yes, ACF. If no, put it in code.

A note on client handoff

Whatever you build, document the field structure. If you’re handing a site to a marketing team, they need to know which fields feed which templates, and what happens if they leave something blank. ACF’s field group export is fine for backup, but a real handoff doc with screenshots is what saves you the 11pm “the homepage is broken” text. For larger builds, that documentation is part of the custom web development work, not an afterthought. I’ve done enough handoffs on Long Island and in the city to know that the sites that survive are the ones where the editor understands the model.

FAQ

Do I still need ACF if I’m using the block editor?

Depends on what you’re building. For core blocks and simple layouts, no. For custom content blocks with structured fields, ACF Blocks are still faster to ship than a native React block. If your blocks need real interactivity, go native.

Yes, for any field group you’re committing to a repo. It removes DB queries on the front end and keeps your field definitions in version control. Just remember: once a field group exists in JSON, don’t also edit it in the admin, or you’ll create a conflict.

Can ACF handle a site with thousands of posts?

Yes, but watch how you query. The fields themselves are just post meta, which scales fine. The problem is calling get_field() in a loop without priming the meta cache, and using meta_query to filter large result sets. Prime the cache, and use taxonomies for filtering where you can.

Have a project like this?

I do custom web development for Long Island businesses. If you need this done right, let’s talk.