
How to Use the WordPress Block Bindings API?
You have a custom field called "event date" on every event post, and you want it to appear under the title in the single event template. In a classic theme, you would add get_post_meta() to a PHP template. In a block theme, templates are HTML, so for years the options were a shortcode, a custom block, or a page builder plugin just to print one value. The Block Bindings API removes that gap. It connects attributes of ordinary core blocks, such as a paragraph's text or an image's URL, to a data source like post meta, so the block displays dynamic data while remaining a normal, stylable block.
This article covers how block bindings work, which blocks and attributes support them in WordPress 7.1, how to bind blocks to custom fields with core/post-meta, how to use the built-in post data and term data sources, how to register your own source in PHP and in the editor with JavaScript, and the rules that keep bound data secure.
How Block Bindings Work
A binding is a small piece of block metadata that says "take this attribute's value from that source." It lives in the block's comment delimiter under metadata.bindings:
<!-- wp-content/themes/my-theme/templates/single-event.html -->
<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"core/post-meta","args":{"key":"event_date"}}}}} -->
<p></p>
<!-- /wp:paragraph -->
This paragraph's content attribute is bound to the core/post-meta source with the argument key: event_date. When WordPress renders the block, it:
- Looks up the registered source,
core/post-meta. - Calls the source's callback with the arguments and the block instance, including context such as the current post ID.
- Replaces the attribute's value in the block's HTML with the returned value.
The paragraph keeps every block feature: typography, colors, spacing, and block style variations all still work. Only the text comes from the data source.
The API arrived in WordPress 6.5 and has grown since:
| Version | What changed |
|---|---|
| 6.5 | Block Bindings API with core/post-meta and core/pattern-overrides |
| 6.7 | Editor APIs for custom sources, and editing post meta through bound blocks |
| 6.9 | core/post-data and core/term-data sources, field lists for the editor UI, and filters for supported attributes |
| 7.1 | List Item content added to the supported attributes |
Supported Blocks and Attributes
Bindings only work on specific attributes of specific blocks, because WordPress has to know how to replace each value in the saved HTML. In WordPress 7.1, the supported list is:
| Block | Attributes that can be bound |
|---|---|
| Paragraph | content |
| Heading | content |
| List Item | content |
| Image | id, url, title, alt, caption |
| Button | url, text, linkTarget, rel |
| Post Date | datetime |
| Navigation Link | url |
| Navigation Submenu | url |
That covers the most common needs: text, links, and images. Everything else, such as a block's background color, is not bindable.
Built-in Sources
| Source | Provides | Arguments |
|---|---|---|
core/post-meta | A registered custom field of the current post | key: the meta key |
core/post-data | The current post's publish date, modified date, or permalink | field: date, modified, or link |
core/term-data | Data about the current term, such as its name or link | field: for example name, link, slug, description, count |
core/pattern-overrides | Per-instance content in synced patterns | None; it uses the block's name in the pattern |
Binding Blocks to Custom Fields with core/post-meta
Step 1: Register the Meta Field
The core/post-meta source only reads meta that is registered and exposed to the REST API. This is a deliberate security rule: unregistered meta, or meta that is not marked show_in_rest, returns nothing.
Register the fields in a small plugin so they survive theme changes:
<?php
/**
* Plugin Name: Event Fields
* Description: Registers event custom fields for block bindings.
* Version: 1.0.0
*/
// wp-content/plugins/event-fields/event-fields.php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
add_action( 'init', 'eventfields_register_meta' );
function eventfields_register_meta() {
$auth = static function ( $allowed, $meta_key, $post_id ) {
return current_user_can( 'edit_post', $post_id );
};
register_post_meta(
'post',
'event_date',
array(
'label' => __( 'Event date', 'event-fields' ),
'type' => 'string',
'single' => true,
'show_in_rest' => true,
'default' => '',
'sanitize_callback' => 'sanitize_text_field',
'auth_callback' => $auth,
)
);
register_post_meta(
'post',
'event_venue',
array(
'label' => __( 'Venue', 'event-fields' ),
'type' => 'string',
'single' => true,
'show_in_rest' => true,
'default' => '',
'sanitize_callback' => 'sanitize_text_field',
'auth_callback' => $auth,
)
);
register_post_meta(
'post',
'event_ticket_url',
array(
'label' => __( 'Ticket URL', 'event-fields' ),
'type' => 'string',
'single' => true,
'show_in_rest' => true,
'default' => '',
'sanitize_callback' => 'esc_url_raw',
'auth_callback' => $auth,
)
);
}
Notes on these arguments:
show_in_restis required for bindings to read the field, and for the editor to load and save it.labelgives the field a readable name in the editor, instead of the raw key.sanitize_callbackcleans values on save. Use a callback that matches the data: text, URL, or a number.auth_callbackcontrols who can change the field through the REST API.- Do not prefix keys with an underscore. Meta keys starting with
_are protected, andcore/post-metarefuses to read them.
If you use a custom post type, replace 'post' with its name, or pass an empty string to register the field for every post type. Registering a post type is covered in how to create a custom post type in WordPress.
Step 2: Bind Blocks in a Template
Now bind blocks to those fields. In the Site Editor, open the single post template or a custom event template, switch to the Code editor from the options menu, and add blocks like these, or put them in your theme's template file:
<!-- wp-content/themes/my-theme/templates/single-event.html -->
<!-- wp:post-title {"level":1} /-->
<!-- wp:group {"layout":{"type":"flex","flexWrap":"wrap"}} -->
<div class="wp-block-group">
<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"core/post-meta","args":{"key":"event_date"}}}}} -->
<p></p>
<!-- /wp:paragraph -->
<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"core/post-meta","args":{"key":"event_venue"}}}}} -->
<p></p>
<!-- /wp:paragraph -->
</div>
<!-- /wp:group -->
<!-- wp:buttons -->
<div class="wp-block-buttons">
<!-- wp:button {"metadata":{"bindings":{"url":{"source":"core/post-meta","args":{"key":"event_ticket_url"}}}}} -->
<div class="wp-block-button">
<a class="wp-block-button__link wp-element-button">Buy tickets</a>
</div>
<!-- /wp:button -->
</div>
<!-- /wp:buttons -->
<!-- wp:post-content {"layout":{"type":"constrained"}} /-->
The button's text is static ("Buy tickets") while its url comes from the field. You can bind several attributes of the same block, or bind only one.
Bound blocks also work inside a Query Loop. Each Post Template iteration passes its own postId through block context, so a list of events can show each event's date and venue without any extra code.
Step 3: Edit the Values
Authors can fill in the fields in two ways:
- In the post editor, directly in the bound block. Since WordPress 6.7, when a post's template or content contains a block bound to
core/post-meta, an author who can edit the post can click the block and type. The editor saves the value to the custom field, not to the block. Empty fields show the field's label as a placeholder. - Through any other interface that writes post meta, such as a custom meta box, the REST API, or WP-CLI.
For testing, WP-CLI is the fastest:
# Terminal
wp post meta update 123 event_date "March 14, 2027"
wp post meta update 123 event_venue "Main Hall, Dhaka"
wp post meta update 123 event_ticket_url "https://tickets.example.com/spring-summit"
In recent versions, the block sidebar also shows an Attributes panel for supported blocks, listing which attributes are connected and to which source. That makes bound blocks easy to spot when someone else edits the template.
Using core/post-data and core/term-data
The two sources added in WordPress 6.9 cover common needs without registering any meta.
A "Read the full story" button that always links to the current post, useful inside a Query Loop card:
<!-- wp-content/themes/my-theme/parts/post-card.html -->
<!-- wp:buttons -->
<div class="wp-block-buttons">
<!-- wp:button {"metadata":{"bindings":{"url":{"source":"core/post-data","args":{"field":"link"}}}}} -->
<div class="wp-block-button">
<a class="wp-block-button__link wp-element-button">Read the full story</a>
</div>
<!-- /wp:button -->
</div>
<!-- /wp:buttons -->
The core/post-data source supports date, modified, and link. Note that modified returns an empty value when the post has never been updated after publishing, which avoids showing a misleading "updated" date equal to the publish date.
In a category or tag archive template, core/term-data can feed a heading with the term's name, or a button with the term's link, using arguments such as {"field":"name"} or {"field":"link"}.
Pattern Overrides: Bindings for Synced Patterns
Synced patterns normally display the same content everywhere. Pattern overrides, powered by the core/pattern-overrides source, let editors change specific blocks inside each instance while keeping the layout synced.
You do not usually write this binding by hand:
- Open a synced pattern for editing.
- Select a Heading, Paragraph, Image, or Button inside it.
- In the block's Advanced settings, enable overrides and give the block a name.
- Save the pattern.
Behind the scenes, WordPress adds a name and a core/pattern-overrides binding to the block's metadata. Each inserted copy of the pattern then stores its own values for those named blocks. Synced patterns and overrides are covered in detail in how to use synced patterns in WordPress.
Registering a Custom Binding Source
Sometimes the data is not post meta: a site-wide setting, a value from a custom table, or something computed. A custom source handles these.
The example below creates a wsm/site-settings source that exposes a small allowlist of site options, such as a support phone number and email, so they can be bound to any paragraph or button and updated in one place.
Server Side: register_block_bindings_source
<?php
/**
* Plugin Name: WSM Site Settings Bindings
* Description: Exposes selected site settings as a block bindings source.
* Version: 1.0.0
*/
// wp-content/plugins/wsm-site-settings/wsm-site-settings.php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Settings that can be bound. Only these keys are ever returned.
*/
function wsm_bindable_settings() {
return array(
'wsm_support_phone' => __( 'Support phone', 'wsm' ),
'wsm_support_email' => __( 'Support email', 'wsm' ),
);
}
add_action( 'init', 'wsm_register_settings_and_source' );
function wsm_register_settings_and_source() {
foreach ( wsm_bindable_settings() as $option => $label ) {
register_setting(
'general',
$option,
array(
'type' => 'string',
'label' => $label,
'show_in_rest' => true,
'default' => '',
'sanitize_callback' => 'sanitize_text_field',
)
);
}
register_block_bindings_source(
'wsm/site-settings',
array(
'label' => __( 'Site Settings', 'wsm' ),
'get_value_callback' => 'wsm_site_settings_get_value',
)
);
}
/**
* Returns the bound value for a block attribute.
*
* @param array $source_args Arguments from the block's binding, e.g. array( 'key' => 'wsm_support_phone' ).
* @param WP_Block $block_instance The block being rendered.
* @param string $attribute_name The attribute being bound, e.g. 'content' or 'url'.
* @return string|null
*/
function wsm_site_settings_get_value( array $source_args, $block_instance, $attribute_name ) {
$key = isset( $source_args['key'] ) ? $source_args['key'] : '';
if ( ! array_key_exists( $key, wsm_bindable_settings() ) ) {
return null;
}
$value = (string) get_option( $key, '' );
// Turn phone and email values into links when bound to a URL attribute.
if ( 'url' === $attribute_name ) {
if ( 'wsm_support_email' === $key && is_email( $value ) ) {
return 'mailto:' . $value;
}
if ( 'wsm_support_phone' === $key ) {
return 'tel:' . preg_replace( '/[^0-9+]/', '', $value );
}
}
return $value;
}
Important details:
- The callback receives three arguments: the binding's
args, theWP_Blockinstance, and the name of the attribute being bound. The third argument lets one source return different formats for text and URL attributes. - Allowlist what the source can return. Without the
array_key_exists()check, anyone who can edit a template could bind a block to any option in the database, including private settings. - Return
nullfor unknown values. The block then renders its original content. - Use
uses_contextwhen you need post data. A source that reads the current post declares'uses_context' => array( 'postId', 'postType' )and reads$block_instance->context['postId']. This source is site-wide, so it needs no context.
WordPress sanitizes bound rich-text values with wp_kses_post() and sets attribute values through the HTML API, which escapes them. You still sanitize on save, as the sanitize_callback does here.
Bind a paragraph and a button to the new source:
<!-- wp-content/themes/my-theme/parts/footer.html -->
<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"wsm/site-settings","args":{"key":"wsm_support_phone"}}}}} -->
<p></p>
<!-- /wp:paragraph -->
<!-- wp:buttons -->
<div class="wp-block-buttons">
<!-- wp:button {"metadata":{"bindings":{"url":{"source":"wsm/site-settings","args":{"key":"wsm_support_email"}}}}} -->
<div class="wp-block-button">
<a class="wp-block-button__link wp-element-button">Email support</a>
</div>
<!-- /wp:button -->
</div>
<!-- /wp:buttons -->
Set the values with WP-CLI, or build a small settings screen:
# Terminal
wp option update wsm_support_phone "+880 1700 000000"
wp option update wsm_support_email "support@example.com"
Editor Side: registerBlockBindingsSource
The PHP registration is enough for the front end. In the editor, a server-only source shows a placeholder rather than the real value. Since WordPress 6.7, you can register the same source in JavaScript to show live values, control whether editors can change them, and, since 6.9, offer a list of fields in the block sidebar.
Enqueue a script for the editor in the same plugin:
<?php
// wp-content/plugins/wsm-site-settings/wsm-site-settings.php (continued)
add_action( 'enqueue_block_editor_assets', 'wsm_site_settings_editor_script' );
function wsm_site_settings_editor_script() {
wp_enqueue_script(
'wsm-site-settings-bindings',
plugins_url( 'bindings.js', __FILE__ ),
array( 'wp-blocks', 'wp-core-data', 'wp-i18n' ),
'1.0.0',
true
);
}
Then create the script. It uses the wp globals, so it needs no build step:
// wp-content/plugins/wsm-site-settings/bindings.js
(function (wp) {
const { registerBlockBindingsSource } = wp.blocks;
const { store: coreStore } = wp.coreData;
const { __ } = wp.i18n;
const FIELDS = {
wsm_support_phone: __("Support phone", "wsm"),
wsm_support_email: __("Support email", "wsm"),
};
registerBlockBindingsSource({
name: "wsm/site-settings",
label: __("Site Settings", "wsm"),
getValues({ select, bindings }) {
// Site settings are only readable by users who can manage options.
const site = select(coreStore).getEntityRecord("root", "site");
const values = {};
for (const [attributeName, binding] of Object.entries(bindings)) {
const key = binding?.args?.key;
values[attributeName] = (site && site[key]) || FIELDS[key] || key;
}
return values;
},
canUserEditValue() {
// Values are managed in settings, not edited inside blocks.
return false;
},
getFieldsList() {
return Object.entries(FIELDS).map(([key, label]) => ({
label,
type: "string",
args: { key },
}));
},
});
})(window.wp);
How the pieces fit:
namemust match the PHP registration exactly.getValuesreturns an object keyed by attribute name. It reads the live settings when the current user can access them, and falls back to the field label so editors still see something meaningful.canUserEditValuereturningfalsemakes bound blocks read-only in the editor. A source that supports editing returnstruewhen appropriate and implementssetValuesto save changes.getFieldsListlets users pick a field from the block sidebar instead of editing block markup by hand.
Extending Supported Attributes
WordPress 6.9 added the block_bindings_supported_attributes filter, plus a per-block variant, block_bindings_supported_attributes_{$block_type}, to add attributes to the supported list. This is mainly useful for custom blocks that you build yourself:
<?php
// wp-content/plugins/my-blocks/my-blocks.php
add_filter( 'block_bindings_supported_attributes_my-plugin/stat', 'myblocks_bindable_stat' );
function myblocks_bindable_stat( $attributes ) {
$attributes[] = 'value';
$attributes[] = 'label';
return $attributes;
}
Adding an attribute to the list only works if WordPress can actually place the value into the block's output. For a dynamic block that renders on the server, read the attribute in your render callback as usual; bound values are resolved before rendering. For static blocks, the attribute must be sourced from the saved HTML. If you are building that kind of block, start with how to build a custom Gutenberg block with block.json and React.
Common Problems and Fixes
- The bound block renders empty. The meta field is not registered, is not
show_in_rest, or its key starts with an underscore. Register it withshow_in_rest => trueand a non-protected key. - Values show in the editor but not on the front end, or the other way round. The source is registered only in JavaScript, or only in PHP. Register it on the server for rendering and in JavaScript for the editor, with identical names.
- A binding to an unsupported attribute is ignored. Only the attributes listed earlier can be bound. Use a supported block, or a custom dynamic block.
- Meta values are blank inside a Query Loop. The source does not declare
uses_context. AddpostIdandpostType, and read the ID from$block_instance->context. - Authors cannot edit a bound field in the post editor. They lack permission for the post, the
auth_callbackdenies them, or the source'scanUserEditValuereturns false. - Private or draft content leaks through a custom source. Check permissions in your callback the way core does: confirm the post is publicly viewable or that the current user can read it, and never return arbitrary options or meta keys.
WordPress Block Bindings API FAQ
It is a core API that connects attributes of blocks, such as a paragraph's text or a button's URL, to a data source like post meta. The block keeps its normal styling while its content is filled in from the source when the page renders.
WordPress 6.5 introduced the API with the post meta and pattern overrides sources. WordPress 6.7 added editor APIs for custom sources, and WordPress 6.9 added the post data and term data sources and field lists in the editor.
The post meta source only reads fields that are registered with show_in_rest set to true and whose keys do not start with an underscore. Register the field with register_post_meta and check that the key in the binding matches exactly.
No. WordPress supports a specific list, including paragraph, heading, and list item content, image URL and alt text, button text and URL, post date, and navigation link URLs. Developers can extend the list for their own blocks with the block_bindings_supported_attributes filter.
They replace the need for shortcodes or custom blocks just to display field values in block themes. You still need a way to manage fields and edit complex data, which is where field plugins or custom meta boxes remain useful.
Yes for post meta since WordPress 6.7, as long as the user can edit the post and the field is registered for the REST API. Custom sources decide this themselves through canUserEditValue and setValues.
Conclusion
The Block Bindings API closes one of the biggest gaps in block themes: showing dynamic data without writing a custom block for every field. Bind a paragraph, heading, list item, image, or button to core/post-meta for custom fields, use core/post-data and core/term-data for links, dates, and term details, and rely on pattern overrides for per-instance content in synced patterns.
When the built-in sources are not enough, register_block_bindings_source() lets you expose any data you control, and registerBlockBindingsSource() brings that data into the editor. Keep sources narrow with allowlists, register meta with show_in_rest and proper sanitization, and check permissions the way core does. The result is a theme where layout stays visual and editable, while the data comes from where it belongs.
Here are some useful references for going deeper on block bindings:
- Block Editor Handbook: Bindings — supported blocks, core sources, and the PHP and JavaScript registration APIs.
- WordPress Developer Resources: register_block_bindings_source() — the server-side function reference.
- WordPress Developer Resources: register_post_meta() — registering meta fields with REST exposure, sanitization, and authorization.
- WordPress Developer Blog: Introducing Block Bindings — a walkthrough of connecting custom fields to blocks.


