All WordPress HTML Templates Forms & Webhooks AI & Tools
WordPress

Custom Fields in 2026: ACF, Meta Boxes and Native Block Bindings

How to handle WordPress custom fields in 2026: Block Bindings API vs ACF, when to pick an ACF alternative, and the postmeta rules that keep sites fast.

A real working moment illustrating the theme of an article about wordpress custom fields. Wide 16:9 banner, one strong focal point, magazine editorial quality, authentic and unstaged.

A client sent us a 40,000 post site last spring with a simple complaint: the editor took eleven seconds to load a product page. The culprit wasn’t the theme. It was a flexible content field with nine layouts, each with its own repeater, stored across roughly 900 rows in wp_postmeta per post. Nobody had done anything wrong exactly. They’d just used the tool the way the tool invites you to use it, for six years.

That site is why we now start every build by asking which WordPress custom fields actually need a field UI, and which are just strings that a block should print. Those are different problems. Since the Block Bindings API landed in WordPress 6.5 they have different solutions too, and a lot of teams are still solving both with the same plugin.

This is the version of the article that assumes you know what a meta box is.

Key Takeaways

  • The Block Bindings API (WP 6.5+, editable meta since 6.7) covers reading and writing single scalar fields into paragraph, heading, image and button blocks. It does not give you a field-group builder, and core has no plans to ship one soon.
  • Meta keys prefixed with an underscore are protected and invisible to the core/post-meta source. If your bindings silently render fallback text, that’s usually why.
  • ACF is still the fastest route to repeaters, flexible content, relationship fields and conditional logic. Its weakness is storage shape, not features: a 5 row by 6 field repeater writes 62 rows to wp_postmeta per post.
  • wppostmeta has indexes on postid and metakey only. There is no index on metavalue, so any filterable attribute belongs in a taxonomy or a custom table, never in a meta query.
  • The 2024 fork of ACF into Secure Custom Fields on WordPress.org means free-tier ACF users are on a different codebase than they think. Check what your client sites are actually running before you plan a migration.

What actually changed, and what didn’t

WordPress 6.5 shipped block bindings with the core/post-meta source. 6.6 added the label argument and tidied the registration signature. 6.7 was the release that mattered: it added client-side source registration through registerBlockBindingsSource, plus canUserEditValue, which is what makes a bound paragraph editable directly in the canvas and writes the value back to post meta. Since then the surface has been stable and the supported attributes have crept outward slowly.

What core still doesn’t give you: a UI for defining a field group. No conditional logic. No repeaters. No options pages. No relationship picker. If you register meta natively you are also writing the editor panel that edits it, unless you’re happy with the ancient Custom Fields panel (still available via the editor Preferences toggle, still a two-column key/value table, still not something you show a client).

So the honest framing for 2026 is not “bindings replaced ACF”. It’s that bindings replaced the template layer. The part of ACF you used to write in PHP inside single-product.php is now markup in a block template. The part you used to click together in the admin is still ACF’s job, or somebody’s.

Registering WordPress custom fields the native way

Everything downstream depends on getting this right. Meta must be registered with showinrest for the editor to see it, and it must not be protected.

add_action( 'init', function () {
    registerpostmeta( 'event', 'event_venue', array(
        'type'          => 'string',
        'single'        => true,
        'default'       => '',
        'showinrest'  => true,           // required for the editor and for core/post-meta
        'label'         => __( 'Venue', 'sc' ), // 6.7+: shown in the bindings picker
        'sanitizecallback' => 'sanitizetext_field',
        'auth_callback' => function () {
            return currentusercan( 'edit_posts' );
        },
    ) );
} );

Three things bite people here. First, single => true is not optional for bindings: array meta returns an array and the block renders nothing useful. Second, the underscore prefix habit we all picked up from meta boxes now actively breaks things, because core/post-meta skips protected keys by design. Rename them. Third, authcallback defaults to editpost_meta capability checks that are stricter than you expect on custom post types with custom capabilities. The failure mode is a field that saves for admins and silently doesn’t for editors.

Binding meta to blocks without writing a block

The payoff is that a template part becomes the entire display layer. This is a paragraph bound to that venue field:

<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"core/post-meta","args":{"key":"event_venue"}}}}} -->
<p>Venue name</p>
<!-- /wp:paragraph -->

Drop that inside a Query Loop and it resolves per post. No PHP, no the_field() calls scattered through a theme. The fallback content between the tags is what renders when the meta is empty, which is a genuinely useful behaviour once you stop thinking of it as placeholder junk and start writing sensible defaults there.

For anything that needs formatting, register your own source. Store the canonical value, format at render:

add_action( 'init', function () {
    registerblockbindings_source( 'sc/price', array(
        'label'              => __( 'Formatted price', 'sc' ),
        'uses_context'       => array( 'postId' ),
        'getvaluecallback' => function ( array $args, $block, $attribute ) {
            $cents = getpostmeta( $block->context['postId'], $args['key'] ?? 'price_cents', true );
            if ( '' === $cents ) {
                return null; // null keeps the block's fallback markup instead of printing empty
            }
            return '£' . number_format( (int) $cents / 100, 2 );
        },
    ) );
} );

That works on the front end immediately and shows nothing in the editor. This is the single most common “bindings are broken” support ticket we see. Sources registered in PHP are server-side only. You need the JS twin for editor preview:

import { registerBlockBindingsSource } from '@wordpress/blocks';
import { store as coreStore } from '@wordpress/core-data';

registerBlockBindingsSource( {
    name: 'sc/price',
    label: 'Formatted price',
    usesContext: [ 'postId', 'postType' ],
    getValues( { select, context, bindings } ) {
        const record = select( coreStore ).getEditedEntityRecord(
            'postType', context.postType, context.postId
        );
        const meta = record?.meta ?? {};
        const values = {};
        for ( const [ attr, binding ] of Object.entries( bindings ) ) {
            const cents = meta[ binding.args?.key ?? 'price_cents' ];
            values[ attr ] = cents
                ? £${ ( cents / 100 ).toFixed( 2 ) }
                : undefined; // undefined falls back to the block's own content
        }
        return values;
    },
    canUserEditValue: () => false, // formatted output, so no inline editing
} );

Keep the two implementations in the same feature folder. When they drift, you get an editor that shows one price and a front end that shows another. You will lose an afternoon to it.

Where native still loses to ACF

Repeaters. That’s the short answer, and it covers about 70 percent of real client requirements. A team roster, an FAQ list, a spec table, opening hours: all repeaters, none of which bindings model at all. You can approximate with an array meta field plus a custom block, but now you’re maintaining a block, an edit component, a save routine and a serializer for something ACF gives a site builder in four clicks.

The other gaps worth naming: conditional field logic (show field B only when A equals X), relationship and post object fields with search, options pages for global settings, gallery fields, and the flexible content layout builder. Core has no equivalent to any of these and won’t for years.

If your custom fields are genuinely flat, and on marketing sites they very often are (subtitle, CTA label, CTA URL, badge text, reading time), skip the plugin entirely. Register the meta, add a small document sidebar panel, and you’ve removed a dependency:

import { registerPlugin } from '@wordpress/plugins';
import { PluginDocumentSettingPanel } from '@wordpress/editor';
import { useEntityProp } from '@wordpress/core-data';
import { useSelect } from '@wordpress/data';
import { store as editorStore } from '@wordpress/editor';
import { TextControl } from '@wordpress/components';

const EventPanel = () => {
    const postType = useSelect( ( s ) => s( editorStore ).getCurrentPostType(), [] );
    const [ meta, setMeta ] = useEntityProp( 'postType', postType, 'meta' );

    return (
        <PluginDocumentSettingPanel name="sc-event" title="Event details">
            <TextControl
                label="Venue"
                value={ meta?.event_venue ?? '' }
                onChange={ ( v ) => setMeta( { ...meta, event_venue: v } ) }
            />
        </PluginDocumentSettingPanel>
    );
};

registerPlugin( 'sc-event-panel', { render: EventPanel } );

Note the import path. PluginDocumentSettingPanel moved from @wordpress/edit-post to @wordpress/editor in 6.6, and the old path throws deprecation warnings you’ll want to clear before a client sees the console.

Choosing an ACF alternative that you won’t regret

Context first, because it changes the calculation. In October 2024 WordPress.org forked Advanced Custom Fields into Secure Custom Fields and took over the plugin slug during the WP Engine dispute. Free-tier sites that auto-updated are now running SCF, not ACF, whether or not anyone noticed. ACF PRO updates come from the vendor directly. Before you plan any migration, SSH in and check what’s actually installed. “We use ACF” is now an ambiguous statement on any site older than 2024.

Our current shortlist, with the reason we’d pick each:

  • ACF PRO: still the default when the client edits fields themselves and repeaters are in scope. Best editor UX, largest hiring pool, native acf/field binding source so field values drop into block templates without shortcodes.
  • Secure Custom Fields: fine for simple free-tier sites. It’s a fork of ACF 6.x free, so field group configs are compatible, but do not assume feature parity going forward.
  • Meta Box: the strongest pick when you’re comfortable defining fields in PHP and want custom table storage. Its custom tables extension is the only mainstream answer to postmeta bloat that doesn’t involve writing your own schema.
  • Carbon Fields: composer-installed, code-only, no admin UI, no licence. If the client will never touch field definitions, this is a clean dependency that ships with your theme and never phones home.
  • Nothing: registerpostmeta plus a sidebar panel plus bindings. For a brochure site with eight flat fields this is less code than configuring a plugin.

Pods and JetEngine both have real users and I’d not talk anyone out of them, but neither has been the right call on a project we’ve run in the last three years.

The postmeta table is where sites go to die

Reading meta is cheap. WordPress loads every meta row for a post in one query and caches it, so getpostmeta() called forty times costs about the same as calling it once. Writing is fine too. The problem is filtering.

wppostmeta ships with two indexes: postid and metakey truncated to 191 characters. There is no index on metavalue. Every meta_query clause adds a JOIN against a table that, on the site I opened this article with, held 36 million rows. Two clauses plus an ORDER BY on a third and you’re into multi-second queries that no object cache saves you from on a cold page.

The rules we hold to:

  1. Anything users filter, sort or facet by goes in a taxonomy, even when it feels wrong semantically. A term relationship query is indexed. A meta value query is not.
  2. Anything with more than about 200 rows per post, or that needs range queries on numbers or dates, goes in a custom table with the indexes you actually need.
  3. Meta is for display data attached to a single known post ID. That’s it.
  4. Repeaters are display data, so they’re allowed, but count the rows first. Five rows times six subfields is 62 postmeta rows because ACF writes a paired underscore reference row for every value.

If the editor load time is your symptom rather than the front end, the fix is usually the same audit. We walk through the measurement side of this in the 2026 WordPress performance playbook, and the meta section is the one people skip and then come back to.

How we decide, on every project

Four questions, in order.

Will the client add or rename fields after launch? If yes, you need a field UI: ACF or Meta Box. If no, code-defined fields are cheaper to maintain and impossible to break through the admin.

Is any field repeatable? If yes, you need a plugin or a custom block. Don’t try to fake a repeater with numbered meta keys. We’ve done it. It’s fine for six months and then someone needs to reorder row three.

Will anything be queried or filtered? If yes, that field is a taxonomy or a table column, regardless of what the rest of the fields do. Mixed storage is normal and correct.

Does the value only get printed into a template? Then bind it. No shortcode, no PHP template override, no custom block. Build the layout once as a synced pattern with the bindings baked in and the whole thing becomes editable furniture. That pairs well with the approach in our guide to building a reusable block pattern library for clients, and it’s the same model CanvasWP uses to keep client-editable sections from turning into a support queue.

Frequently Asked Questions

Can the Block Bindings API replace ACF completely?

Only for flat, single value fields. Bindings read and write post meta into a limited set of block attributes, but they provide no field group interface, no repeaters and no conditional logic. On a brochure site with a dozen text and image fields, yes, you can drop the plugin. On anything with repeatable content, you can’t.

Why is my bound block showing placeholder text instead of the meta value?

Check three things in order: the meta key does not start with an underscore (protected meta is excluded from core/post-meta), the meta is registered with showinrest => true and single => true, and the registration runs on init for the correct post type. If the value appears on the front end but not in the editor, you’ve registered a custom source in PHP without the matching registerBlockBindingsSource call in JavaScript.

Is Secure Custom Fields the same as ACF?

It started as a fork of the free version of ACF in October 2024 and remains config-compatible with existing field groups, but it is a separate project with separate maintainers. ACF PRO features are not in it, and the two codebases will keep diverging. Audit which one your sites are actually running before you assume anything about upgrade paths.

How many custom fields is too many for one post?

There’s no hard limit, and reading them is cheap because WordPress caches the whole meta set per post in a single query. The pain starts when you filter on them or when row counts push into the hundreds per post, which slows the editor’s REST save payload and bloats the database. Past roughly 200 rows per post, move the heavy data to a custom table.

Should I migrate an existing ACF site to native block bindings?

Usually not as a project in itself, because the migration risk outweighs the dependency saving. Do it opportunistically: when you rebuild a template, replace the the_field() calls with bindings and leave the field definitions in ACF. ACF’s own acf/field binding source lets you do exactly that without touching storage.

The one decision to make this week: open your largest client site, run a count on wppostmeta grouped by metakey, and look at the top twenty. If any of those keys appear in a meta_query anywhere in your theme, you’ve found your next refactor. Everything else in this article is optimisation. That one is a bug waiting for traffic.

acf alternative for wordpress 2026 acf repeater postmeta bloat block bindings api wordpress tutorial register_post_meta show_in_rest bindings registerBlockBindingsSource javascript example secure custom fields vs acf fork wordpress custom fields wp_postmeta meta_query performance