WordPress Block Bindings API: Connecting Block Attributes to Data Sources

The Block Bindings API (stable in WordPress 6.5) lets you connect core block attributes — content on Paragraph, url/alt on Image, href/text on Button — directly to custom data sources registered in PHP, without writing a custom dynamic block. This is the FSE equivalent of ACF’s display functions: your data source provides the value, the core block renders it with all its existing editor controls.

Problem: Block attributes are connected to post meta, site options, or taxonomy terms via JavaScript in the block's edit component — but this creates duplicate data sources and means the block cannot read live data outside the editor without custom REST endpoints.

Solution: Use the WordPress Block Bindings API (WordPress 6.5+) — register a custom binding source with register_block_bindings_source(), define a get_value callback that reads from any server-side data source, and bind a block attribute to the source with "metadata": {"bindings": {"content": {"source": "my/source"}}} in the block's attributes. The block reads live data on every render.


The code below registers a custom binding source that reads post meta, shows the block markup with metadata.bindings, and demonstrates a compound binding that provides multiple attributes from a single data source call.


 __( 'Post Meta', 'my-plugin' ),
        'get_value_callback' => function ( array $source_args, WP_Block $block ): mixed {
            $meta_key = sanitize_key( $source_args['key'] ?? '' );
            if ( ! $meta_key ) {
                return null;
            }
            $post_id = $block->context['postId'] ?? get_the_ID();
            $value   = get_post_meta( $post_id, $meta_key, true );
            return $value ?: null;
        },
        'uses_context'       => [ 'postId', 'postType' ],
    ] );

    // Binding source for external REST data (cached)
    register_block_bindings_source( 'my-plugin/stock-price', [
        'label'              => __( 'Stock Price', 'my-plugin' ),
        'get_value_callback' => function ( array $source_args ): ?string {
            $ticker = strtoupper( sanitize_text_field( $source_args['ticker'] ?? '' ) );
            if ( ! $ticker ) {
                return null;
            }
            $cache_key = 'stock_' . $ticker;
            $price     = get_transient( $cache_key );
            if ( false === $price ) {
                $response = wp_remote_get( "https://api.stocks.example.com/price/$ticker" );
                if ( ! is_wp_error( $response ) ) {
                    $data  = json_decode( wp_remote_retrieve_body( $response ), true );
                    $price = '$' . number_format( (float) ( $data['price'] ?? 0 ), 2 );
                    set_transient( $cache_key, $price, 5 * MINUTE_IN_SECONDS );
                }
            }
            return $price ?: null;
        },
    ] );
} );


{
    "name": "core/paragraph",
    "attributes": {
        "content": "",
        "metadata": {
            "bindings": {
                "content": {
                    "source": "my-plugin/post-meta",
                    "args": { "key": "_product_subtitle" }
                }
            }
        }
    }
}


{
    "name": "core/image",
    "attributes": {
        "url": "",
        "alt": "",
        "metadata": {
            "bindings": {
                "url": { "source": "my-plugin/post-meta", "args": { "key": "_hero_image_url" } },
                "alt": { "source": "my-plugin/post-meta", "args": { "key": "_hero_image_alt" } }
            }
        }
    }
}


NOTE: Block Bindings only work on supported attributes of supported core blocks (Paragraph→content, Image→url/alt/title, Heading→content, Button→url/text/linkTarget) — you cannot bind arbitrary attributes on custom blocks using this API; for custom blocks, use render_callback with get_post_meta() directly instead.