The Block Bindings API connects a stored value — usually a custom field — to an attribute on a core block, so a Paragraph can print post_meta with no shortcode, no custom block, and no PHP in a template file. It landed in WordPress 6.5 and it is the machinery underneath pattern overrides, so if you have ever marked a block as overridable inside a synced pattern, you have already shipped a binding.
What it is not: a field UI, a templating language, or a way to render a repeater. It swaps one value into one attribute at render time. That narrowness is the whole reason it is reliable, and it is also the reason half the people who try it give up on day two.
The sixty-second version
show_in_rest => true, 'type' => 'string', and no leading underscore. Select a Paragraph, Heading, Image or Button in the editor, open the Attributes panel in the block inspector, and pick the field. That is the entire happy path. If your field does not appear in that list, it is almost always one of those three registration details — jump to Connect a custom field below.What the Block Bindings API actually does
A binding is a note in the block’s comment that says “when you render this attribute, go and ask this source for the value instead”. Nothing more. Here is a Paragraph bound to a meta key:
<!-- wp:paragraph {"metadata":{"bindings":{
"content":{"source":"core/post-meta","args":{"key":"wpfairs_reading_time"}}
}}} -->
<p>About 5 minutes</p>
<!-- /wp:paragraph -->
Three things are worth noticing in that snippet, because each one explains a behaviour that surprises people later.
The bindings live under metadata, keyed by attribute name. One block can bind several attributes to several sources — an Image can take its url from one field and its alt from another, independently.
The HTML between the delimiters is still there. “About 5 minutes” is not dead weight; it is what renders if the source returns nothing, and it is what the block falls back to in contexts where the source cannot resolve. Treat it as a deliberate default rather than leftover placeholder text.
The substitution happens at render, in PHP. The stored markup never changes when the field changes. That is good for the obvious reason — edit the custom field and every post using the binding updates — and it also means bound blocks sidestep the usual static-block validation problem entirely. If you have fought with a block that contains unexpected or invalid content, the mismatch there is between stored HTML and a save function; a bound attribute is resolved after all that, so it is not part of the comparison.
Which blocks and attributes support bindings
This is the first wall most people hit, so check it before you design anything around the API. The list is short and deliberate. On a narrow screen the table stacks into one card per block.
| Block | Bindable attributes | What that gets you in practice |
|---|---|---|
core/paragraph | content | The workhorse. Any single string — a price, a model number, a review date |
core/heading | content | A per-post subtitle or product name without a custom template |
core/image | id, url, title, alt, caption | The most useful one. Bind url and alt to two separate fields and you have a per-post hero that editors cannot style wrong |
core/button | url, text, linkTarget, rel | A per-post CTA — an affiliate link, a booking URL — with the design locked in the pattern |
core/list-item | content | Added in WordPress 7.1, along with a fix that stops a nested sub-list being dropped when a bound item renders |
core/post-date | datetime | Show a “last reviewed” date from a field instead of the post’s modified date |
core/navigation-link, core/navigation-submenu | url | Narrow, but it is how you drive a menu destination from a stored setting |
Since WordPress 6.9 you can widen that list yourself with the block_bindings_supported_attributes filter — the Handbook’s own example adds linkDestination to the Image block. It does not let you make a block bindable that is not already on the list, so it extends the table above rather than replacing it.
Pattern overrides support fewer blocks than bindings do
core/pattern-overrides source, but they are offered on four block types only: Heading, Paragraph, Image and Buttons. List Item and Post Date support bindings without being offered as overrides. If you are choosing a reuse mechanism rather than a data source, synced patterns vs template parts works through that decision separately.Connect a custom field to a block in four steps
1. Register the meta key properly
An unregistered meta key does not exist as far as the core/post-meta source is concerned. Put this in a small site plugin rather than in functions.php, so the field survives a theme change:
<?php
add_action( 'init', function () {
register_post_meta( 'post', 'wpfairs_reading_time', array(
'show_in_rest' => true,
'single' => true,
'type' => 'string',
'default' => '',
'sanitize_callback' => 'wp_strip_all_tags',
'auth_callback' => function () {
return current_user_can( 'edit_posts' );
},
) );
} );
Each argument there is load-bearing. show_in_rest is what exposes the value to the editor at all — without it the field is invisible to the block editor and to the REST API. single means one value rather than an array, which is what a bound attribute can consume. 'type' => 'string' is the type the editor’s field list offers; register it as an integer or an array and it will not appear. And the first argument is the post type, so repeat the call for each post type that needs the field.
Underscore-prefixed keys are off limits to core/post-meta
_ is protected meta, and the core/post-meta source will not read it — this is the single most common reason a binding silently renders the fallback text. It is also why so much existing plugin data is unreachable this way: most plugins prefix their keys with an underscore deliberately, to keep them out of the old Custom Fields box. You can still read those values, but you have to register your own source, which the next section covers.2. Give the field somewhere to be edited
Registering meta creates storage, not a text box. You need one of: the classic Custom Fields panel (enabled from the editor’s Preferences → General), a field plugin, a small block editor plugin of your own, or — often the quickest — nothing at all, because the bound block itself is editable when the source allows it and the current user has permission. For the core/post-meta source, typing into a bound Paragraph writes back to the field.
3. Bind the attribute
Select the block, open the block inspector, and find the Attributes panel. Choose the attribute, then choose the field. The editor writes the metadata.bindings JSON for you.
You can also hand-write it in the Code editor (Ctrl/Cmd + Shift + Alt + M), and for a template file you will have to. Get the JSON wrong — a trailing comma, an unquoted key, a missing brace — and the editor reports the block as invalid rather than telling you the binding is malformed, which sends people hunting in the wrong place entirely.
4. Check what happens when the field is empty
Do this deliberately, on a post where the field has never been filled in. With no value, the editor shows a placeholder and marks the block as connected rather than letting you type into it freely; on the front end you get the registered default if you set one, and the stored fallback HTML otherwise. An empty bound Image is the case to test hardest, because an unset url is visibly different from an unset paragraph.
Bindings inside a Query Loop
This is the part that makes the API worth the setup, and it is easy to miss. The core/post-meta source is registered with uses_context of postId and postType, so a bound block inside a Query Loop resolves against whichever post that iteration is rendering — not against the page the loop sits on.
Practically: build one card layout in a Query Loop template, bind a Heading to a subtitle field and a Button’s url to a link field, and every card fills itself in. The same markup works in a single template, an archive template, or a synced pattern dropped into a post. If you have been building that kind of thing with a custom block or a shortcode, this is the replacement, and it keeps the layout editable in the Site Editor instead of hiding it in PHP. Where that markup belongs in a block theme is its own question — customising a block theme with theme.json or a child theme covers the cascade.
Registering your own binding source in PHP
Your own source is the answer to every “but my data is not a plain post meta string” problem: protected keys, options, term meta, a formatted date, a computed value, a field inside a serialised array. Register it on init:
<?php
add_action( 'init', function () {
register_block_bindings_source( 'wpfairs/last-reviewed', array(
'label' => __( 'Last reviewed', 'wpfairs' ),
'uses_context' => array( 'postId' ),
'get_value_callback' => function ( $source_args, $block_instance, $attribute_name ) {
$post_id = $block_instance->context['postId'] ?? get_the_ID();
if ( ! $post_id ) {
return null;
}
// A protected key, which core/post-meta refuses to read.
$stamp = get_post_meta( $post_id, '_wpfairs_reviewed_gmt', true );
if ( ! $stamp ) {
return null;
}
$format = $source_args['format'] ?? get_option( 'date_format' );
return wp_date( $format, strtotime( $stamp ) );
},
) );
} );
And in the content or template:
<!-- wp:paragraph {"metadata":{"bindings":{"content":{
"source":"wpfairs/last-reviewed","args":{"format":"j F Y"}
}}}} -->
<p>Not yet reviewed</p>
<!-- /wp:paragraph -->
Four details from the function reference that save a debugging session:
If a binding renders nothing and you cannot tell why, the callback is ordinary PHP running at render time, so a log line inside it is the fastest diagnosis — enabling WordPress debugging and reading error logs safely covers doing that without printing notices to visitors. There is also a block_bindings_source_value filter, added in 6.7, if you need to adjust a value a source you do not own has returned.
Bindings, a field plugin, or a custom block?
Here is what I weigh before picking one on a client build. Disagree with the weighting rather than the conclusion — your editors and your deployment setup should move it.
Judged on those criteria, the three approaches land like this. Stacks into one card per approach on a phone.
| Core bindings + registered meta | A field plugin’s binding source | Custom block with render_callback | |
|---|---|---|---|
| What you write | One register_post_meta() call | Field definitions in the plugin’s UI | block.json, an edit component, a PHP render callback, a build step |
| Field UI for editors | The Custom Fields box, or none — you build it | Labelled, validated, conditional fields out of the box | Whatever you build in the inspector |
| Data shapes | Single strings | Scalars only for bindings; richer types still need PHP. ACF explicitly excludes Repeater, Flexible Content, Gallery, Relationship and Checkbox | Anything |
| Control over output HTML | None beyond the core block | None beyond the core block | Total |
| Inside a Query Loop | Yes, via postId context | Yes, same mechanism | Yes, if you read context |
| If the plugin is deactivated | Not applicable — it is core | Binding stops resolving; fallback text renders | Block is unregistered; editor shows a missing-block notice |
| In version control | Yes, it is a few lines of PHP | Field groups are in the database unless you export them to files | Yes |
| Where it breaks down | No field UI, no repeaters, eight blocks | You are tied to the plugin’s release cycle and its supported field types | Maintenance. It is a JavaScript project now |
If you already run a field plugin, check whether it ships a source before writing your own. ACF added full support in 6.8.1 under the source name acf/field, taking the field name as args.key, and edits made in the block sync back to the field provided it has “Allow Access to Value in Editor UI” switched on. Meta Box registers four sources — meta-box/post-field, plus term, author and settings-page variants that need the matching extensions. Pods has pods/bindings-field, added in Pods 3.2. None of these replaces the core source; they sit alongside it, and a site can use several at once.
Where the Block Bindings API falls short
Worth knowing before you build a content model on it rather than after.
So should you use it?
- One or two scalar fields per post, shown in a fixed spot: yes, and do not install anything. Register the meta, bind the block, done in twenty minutes.
- You already have ACF, Meta Box or Pods and a pile of existing fields: yes, through that plugin’s source. You keep the field UI your editors know and stop writing
the_field()calls into templates. - A card layout repeated across a Query Loop: this is the strongest case on the page. One template, context-aware bindings, no per-post markup.
- Repeaters, galleries, or anything that renders a list: no. Write a block with a
render_callbackand spend the effort there instead. - Data that lives in protected
_-prefixed meta from another plugin: yes, but you are writing a custom source. Budget an hour, and check the plugin is not already shipping one. - You maintain a block library and want users to bind your blocks: not yet. There is no supported way in.
One habit worth adopting: keep every register_post_meta() and register_block_bindings_source() call in one small plugin file, with a comment above each saying which template or pattern consumes it. Bindings are invisible from the admin — there is no screen listing which blocks use which field — so that file is the only documentation anyone will have when a field needs renaming in two years.
Frequently asked questions
Which WordPress version do I need for block bindings?
WordPress 6.5 introduced the API. 6.7 added the editor-side registration functions and the block_bindings_source_value filter. 6.9 added the core/post-data and core/term-data sources, getFieldsList for the Attributes dropdown, and the block_bindings_supported_attributes filter. WordPress 7.1 added List Item support. On anything older than 6.5 the metadata.bindings JSON is simply ignored.
Why does my custom field not appear in the Attributes panel?
Four things to check, in this order: the meta is registered with show_in_rest => true; the key does not start with an underscore; it is registered as 'type' => 'string'; and it is registered for the post type you are editing. Fix those and it appears without any further work. If the key must stay protected, register your own source instead.
Can editors change the value from inside the bound block?
With the core/post-meta source, yes, for users with permission to edit that meta. For a custom source it depends entirely on how it was registered: a PHP-only source is read-only, and editing requires a JavaScript registration supplying setValues and canUserEditValue. ACF supports write-back for fields with “Allow Access to Value in Editor UI” enabled.
Do bindings work inside a Query Loop or an archive template?
Yes. The core/post-meta source declares uses_context of postId and postType, so each iteration of the loop resolves against its own post. Your own source gets the same treatment if you list the context keys you need in uses_context and read them from $block_instance->context.
Does the Block Bindings API replace ACF?
It replaces the display half, not the authoring half. Bindings remove the need for template PHP that echoes a field, which is a real saving. They do nothing about defining labelled, validated, conditional fields in an interface editors can use — which is what a field plugin is actually for. Most sites end up using both, with the plugin providing the fields and its binding source providing the display.
Can I bind a custom field to a block from a third-party block library?
No. Bindings are supported on a fixed list of core blocks, and there is no public way for a block to opt in. The block_bindings_supported_attributes filter adds attributes to blocks already on that list. If you need dynamic data in a third-party block, your options are that block’s own dynamic-content feature, if it has one, or replacing it with a core block you can bind.
Will bindings slow down my site?
The core post-meta source is a get_post_meta() call, which is already cached per post, so the cost is negligible. Your own source is as fast as you write it: it runs once per bound attribute per render, with no caching layer in front of it, so a remote request or an uncached query inside the callback is the thing to watch — particularly on an archive page rendering thirty cards.
Sources
- Bindings — Block Editor Handbook, for the supported blocks and attributes, the
metadata.bindingsformat, thecore/post-metarequirements, and the editor-side registration arguments - register_block_bindings_source() — the signature, the namespace rule, the three callback arguments, and the string /
null/falsereturn behaviour - _register_block_bindings_post_meta_source() — the core source’s
uses_contextofpostIdandpostType, which is what makes Query Loop bindings work - Introducing Block Bindings, part 1: connecting custom fields — the
register_meta()arguments and the empty-value behaviour in the editor - WordPress 7.1 Source of Truth — List Item block bindings support and the nested sub-list fix
- ACF: Block Bindings — the
acf/fieldsource, the 6.8.1 requirement, the supported and unsupported field types, and the editor write-back rule - Connect Meta Box Fields to Gutenberg Blocks with Block Bindings — the four Meta Box source names and the extensions each one needs
- Pods Block Bindings API — the
pods/bindings-fieldsource and its arguments, since Pods 3.2 - The WordPress Block Bindings API — Kinsta, for the string-type requirement on meta shown in the Attributes field list

Leave a Reply to This Post