Intro to the Block Bindings API

By Ian Svoboda on May 13, 2026

If you’ve ever built a custom block just to display a single piece of dynamic data, the Block Bindings API is the feature you didn’t know you were waiting for. You can use this new API to bind dynamic data to specific block attributes.

The classic WordPress request: a client wants to show a custom field (subtitle, estimated read time, last-updated date) next to a post title. For years, you had three options. Build a custom dynamic block. Install ACF and use ACF Blocks. Or some combination of the two.

Since WordPress 6.5, there’s been a new option available: the Block Bindings API. This allows you to bind data to a specific block attribute such as the content of the Paragraph block, the url of an Image block, and more.

If you’ve been holding off on looking into the Block Bindings API, this is the post for you. We’ll cover what options there are right now and go over a few practical examples.

How block bindings work

Block bindings pull data from a given source into a specific block attribute. WordPress registers a handful of binding sources you can use immediately:

  • core/post-meta: bind to any registered post meta field on the current post.
  • core/post-data: data about the current post.
  • core/term-data: data about the current term.
  • core/pattern-overrides: used by synced patterns to allow per-instance overrides.1

If you want to bind data that isn’t covered in the above sources (e.g. user meta, term meta, options, etc.), you’ll need to register a custom source (more on this below).

By default, you can only bind things to certain attributes in a specific allowed list of blocks:

  • Paragraph: content
  • Heading: content
  • Image: url, alt, title, id, caption
  • Button: text, url, linkTarget, rel

See the Block Editor Handbook for the latest list of allowed blocks.

As of WordPress 6.9, you can filter the block attributes available for binding to include any other attribute or even the attributes of custom blocks. Any block attributes that are available for binding also support synced pattern overrides since they rely on the same underlying mechanism.

See the docs for an example of filtering the attributes.

The simplest working example

Let’s show a “subtitle” custom field underneath a post title.

First, register the meta key using core WordPress functions:

PHP
<?php
add_action( 'init', function () {
    register_post_meta( 'post', 'subtitle', array(
        'type'          => 'string',
        'single'        => true,
        'show_in_rest'  => true,
        'auth_callback' => function () {
            return current_user_can( 'edit_posts' );
        },
    ) );
} );

Two parts of that snippet matter:

  1. The show_in_rest flag is required for the meta key to appear in the bindings UI.
  2. The auth_callback controls who can edit the value, which the binding system respects (a user without permission won’t see editable bindings for that key).

Now in the editor, insert a Paragraph block, open the block inspector, and look for the “Attributes” panel near the bottom of the Block tab. Click the icon next to content, choose Post Meta, and select subtitle from the list.

What you get on the page is real block markup:

HTML
<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"core/post-meta","args":{"key":"subtitle"}}}}} -->
<p>Subtitle placeholder</p>
<!-- /wp:paragraph -->

The metadata.bindings object is the binding declaration. On the frontend, WordPress reads it, calls the source’s callback, and replaces the paragraph’s inner content with the meta value. You don’t have to do any extra filters on render_block or any other output to make it work.

A few things to keep in mind

Block bindings are cool, but there are a few rough edges worth knowing about before you commit to bindings.

Meta keys need show_in_rest. If the key isn’t registered with REST support, it won’t appear in the bindings UI. The binding will still work if you hand-write the markup, but you’ve lost the editor UX you wanted in the first place.

Values must be scalars. Strings, numbers, booleans. The binding system reads a single value and stuffs it into a single attribute. Arrays, objects, and serialized data are not supported by for output, but you can use a custom source to transform them into a simple scalar value. For instance, you might take an array and output it as a comma separated string.

Permissions are enforced. The auth_callback on register_post_meta() controls whether the current user can edit the value. If they can’t, the editor shows the binding but the field is read-only.

A bound block becomes uneditable in the editor. This catches people. If a Paragraph’s content is bound to meta, you can’t type into the paragraph anymore. You edit the meta value (in the post’s sidebar, in a custom panel, or via the field’s UI), and the binding pulls it in. For displaying values, this is fine. If you wanted a block that’s editable and shows dynamic content, bindings aren’t the right tool.

Empty values fall back to the block’s saved content. If the meta key has no value, the bound attribute keeps whatever was originally inside the block (the Subtitle placeholder text from the earlier example). That’s usually fine, but it’s worth knowing when you’re styling around it, especially if you’d rather hide the block entirely when there’s nothing to show.

When you need a custom source

The built-in sources cover a lot, but not everything. The cases where you’ll need to register your own source:

  • Computed values. Word count, reading time, average rating from child posts.
  • External data. Latest commit, current weather, exchange rates.
  • Data outside post meta. Term meta, user meta, options table values.
  • Anything that takes arguments. A source where the binding decides which option to read, for example.

This is where custom binding sources come in, and thankfully, they’re more approachable than you’d expect.

Building a custom source: reading time

Let’s register a myplugin/reading-time source that calculates the estimated reading time from the current post’s content.

PHP
<?php
add_action( 'init', function () {
    register_block_bindings_source(
        'myplugin/reading-time',
        array(
            'label'              => __( 'Reading Time', 'myplugin' ),
            'get_value_callback' => 'myplugin_reading_time_callback',
            'uses_context'       => array( 'postId' ),
        )
    );
} );

function myplugin_reading_time_callback( $source_args, $block_instance, $attribute_name ) {
    $post_id = $block_instance->context['postId'] ?? get_the_ID();

    if ( ! $post_id ) {
        return '';
    }

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

    return sprintf(
        /* translators: %d: estimated reading time in minutes. */
        _n( '%d minute read', '%d minutes read', $minutes, 'myplugin' ),
        $minutes
    );
}

A few things worth pointing out in that callback.

The uses_context array declares that this source needs the postId of the rendering context. WordPress passes that context into the block instance, which is how you reach the right post when the binding runs inside a Query Loop. Without uses_context, you’d be guessing.

The $source_args parameter is for arguments passed in the binding declaration itself. We’re not using any here, but if you wanted a myplugin/option source that took an option name, that’s where it would arrive.

The callback returns a plain string. WordPress takes care of putting it into the bound attribute and rendering the block. No template work on your end.

To actually use the source, you’ll write the binding into the block markup (most custom sources don’t ship an editor UI by default, which is a longer topic for a follow-up post):

HTML
<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"myplugin/reading-time"}}}} -->
<p>Reading time placeholder</p>
<!-- /wp:paragraph -->

That’s the whole feature. Register the source, write the callback, drop the binding into your block markup. The frontend renders the value, and the block editor shows the placeholder until you preview the post.

When not to reach for bindings

Block bindings are nice, but they do have some limitations that might be a deal breaker in some situations.

Specifically, bindings are the wrong tool when:

  • You need rich-text editing on a block that’s also showing dynamic content. Bound blocks aren’t editable in the editor, so you can’t type in extra content around the bound value.
  • The thing you’re building is interactive and needs frontend JavaScript. Bindings are a server-side rendering feature.

Wrapping up

The Block Bindings API quietly replaced a large category of “build a custom block to show one value” work. While you can use block bindings with custom blocks, you don’t have to in more straightforward use cases.

You can register post meta in three lines, bind a core block to it, and ship. For cases that core/post-meta or another built-in source doesn’t cover, custom sources are short, server-side, and don’t require any JavaScript.

Happy coding!

Further Reading

  1. Synced patterns are patterns that are the same block markup everywhere they’re used. However, you can override specific attributes on certain blocks in a synced pattern. This allows you to keep a block set up a specific way but only allow the text to be changed, among other options. ↩︎


Leave a Reply

Your email address will not be published. Required fields are marked *