Block Bindings

Using the Block Bindings API

⏱ 17 min

The block you built in the previous lesson stores its own content directly as a block attribute. The Block Bindings API, shipped in WordPress 6.5, offers a different shape entirely: connecting a block attribute to a value that lives somewhere else and is computed at render time, post meta, a pattern override, or a source you register yourself. This lesson covers the API on its own terms, using a non-AI example, before the next lesson builds a source that returns an AI-generated value specifically.

What you'll learn in this lesson
register_block_bindings_source()
The function signature and its required label and get_value_callback arguments.
What get_value_callback actually receives
source_args, the block instance, and the attribute name, in that order.
uses_context
How a source reads values like the current postId from the block tree around it.
The real block markup shape
How metadata.bindings shows up in a block's saved comment delimiters.
Prerequisites

A local WordPress 6.5 or newer environment. No specific block from the previous lesson is required, this lesson’s example uses a plain core paragraph block.

Step 1: What a binding source actually is

A block binding connects one attribute of one block instance to a value supplied by a named source, instead of a value typed directly into that attribute. WordPress core ships two built-in sources: core/post-meta, which binds to a post meta field, and core/pattern-overrides, used for editable regions inside synced patterns. Registering your own source with register_block_bindings_source() lets any block attribute pull its value from wherever your plugin decides, computed fresh each time the block renders.

// File: my-plugin/my-plugin.php
add_action( 'init', function () {
	register_block_bindings_source(
		'my-plugin/reading-time',
		array(
			'label'              => __( 'Estimated Reading Time', 'my-plugin' ),
			'uses_context'       => array( 'postId' ),
			'get_value_callback' => 'my_plugin_get_reading_time',
		)
	);
} );

Sources are always registered on init, the same hook block types themselves use, not on a separate hook the way abilities are. The source name follows the same namespacing convention as everywhere else in WordPress: your-plugin-slug/source-name.

Step 2: get_value_callback, argument by argument

The callback receives up to three arguments: the source-specific $source_args from the block’s own markup, the $block_instance (a WP_Block object, which is how uses_context values become available), and the name of the attribute currently being resolved:

// File: my-plugin/my-plugin.php
function my_plugin_get_reading_time( $source_args, $block_instance, $attribute_name ) {
	$post_id = $block_instance->context['postId'] ?? null;

	if ( ! $post_id ) {
		return null;
	}

	$word_count = str_word_count( wp_strip_all_tags( get_post_field( 'post_content', $post_id ) ) );
	$minutes    = max( 1, (int) round( $word_count / 200 ) );

	return sprintf(
		/* translators: %d: number of minutes. */
		_n( '%d minute read', '%d minute read', $minutes, 'my-plugin' ),
		$minutes
	);
}

$block_instance->context['postId'] only exists because uses_context declared postId as something this source needs. Without listing it in uses_context, that key simply won’t be present in $block_instance->context, this is the same context mechanism ordinary blocks use to read values like the current post ID from an ancestor block, not something specific to bindings.

Returning null is the correct way to say “no value available,” WordPress falls back to the attribute’s default in that case rather than rendering a broken string.

Step 3: The markup shape a bound attribute produces

Once an attribute is bound, the block’s saved comment delimiter grows a metadata.bindings entry describing the connection, instead of the attribute’s plain value:

<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"my-plugin/reading-time"}}}} -->
<p>1 minute read</p>
<!-- /wp:paragraph -->

The shape is always metadata.bindings.{attribute_name}.source, with an optional sibling args key for sources that need extra parameters, exactly how core’s own core/post-meta source passes which meta key to read:

<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"core/post-meta","args":{"key":"subtitle"}}}}} -->
<p>Fallback text shown before the binding resolves</p>
<!-- /wp:paragraph -->

The plain text sitting inside the block markup itself is only a fallback, once the binding resolves at render time, get_value_callback’s return value replaces it.

Test it

The fastest way to see a custom source resolve, without building any editor UI for it yet, is the block editor’s Code Editor view. Add the PHP from Steps 1 and 2 to a plugin file, open any post, switch to Code Editor (the ”⋮” options menu, or Ctrl+Shift+Alt+M), paste in a paragraph block with the metadata.bindings markup from Step 3, and switch back to the visual editor. The paragraph should display the real, computed reading time text, not the fallback text you typed.

A bound attribute becomes read-only in the visual editor

Once an attribute like content is connected to a binding source, WordPress locks it against direct typing in the visual editor, you’ll see a small icon on the block indicating it’s connected, and editing that field directly is disabled. This is expected, the value is meant to come from the source, not from the author retyping it. If you need an editable UI to attach and detach bindings rather than hand-editing markup, that’s a separate piece of editor UI you’d build yourself, core provides the attribute-locking behavior but not a polished connect/disconnect interface for every custom source out of the box.

Recap

register_block_bindings_source() takes a namespaced source name, a required label and get_value_callback, and an optional uses_context array for reading values like the current post ID from the surrounding block tree. get_value_callback receives $source_args, the $block_instance, and the attribute name, and its return value replaces the block’s static content at render time. The markup shape, metadata.bindings.{attribute}.source (plus optional args), is the same shape core’s own core/post-meta and core/pattern-overrides sources use. The next lesson puts an AI-generated value behind exactly this mechanism.

Resources & further reading

← Building Your First AI-Powered Block Building a Custom AI Block Bindings Source →