WordPress Block Bindings & Patterns: Best Practices for 2026
How to use the Block Bindings API and theme.json patterns the way the WordPress core team designed them — without painting yourself into a corner.
The Block Bindings API landed in WordPress 6.5 and quietly became the most important authoring feature in years. By WordPress 6.8 it's stable, well-documented, and supported by every modern theme — including ours. This post is what we wish someone had told us when we first migrated.
What block bindings actually solve
A block binding is a contract between a block's attribute and a data source. The block stays generic — a core/paragraph, core/image, core/heading — and a binding makes its content come from somewhere else: post meta, a custom field, a settings panel, an API.
The old way to do this was a hand-rolled core/block with serverSide rendering or a custom block. Both were brittle. With bindings, the editor preview, the front-end, and the block-pattern transformer all use the same data source automatically.
The minimum-viable binding source
In your plugin or functions.php:
add_action('init', function () {
register_block_bindings_source('themescorners/author-bio', [
'label' => __('Author bio', 'themescorners'),
'get_value_callback' => function ($args, $block) {
$userId = get_the_author_meta('ID');
return get_user_meta($userId, 'bio', true);
},
]);
});
In a block pattern or content:
<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"themescorners/author-bio"}}}} -->
<p>placeholder bio</p>
<!-- /wp:paragraph -->
That's it. The paragraph renders the author's bio on the front-end and shows it (with editing locked) in the editor.
Best practice #1 — bind, don't custom-block
Every time you reach for registerBlockType, ask first: could this be a core/group with bindings? In our experience that question kills about 70% of custom blocks. Less custom code, less editor risk, full pattern compatibility.
Best practice #2 — keep callbacks under 1ms
get_value_callback runs once per binding per render. Inside a loop of 20 posts with 3 bindings each, that's 60 calls. If each one does a get_post_meta() without caching, you've added 60 SQL queries to a single page. Cache aggressively:
'get_value_callback' => function ($args, $block) {
static $cache = [];
$postId = get_the_ID();
return $cache[$postId] ??= get_post_meta($postId, '_tc_price', true);
},
Best practice #3 — use theme.json for patterns, not PHP
In 2026 the canonical place to ship patterns is theme.json plus /patterns/*.php files. Skip register_block_pattern() in PHP unless you have a genuine dynamic need.
// patterns/hero-with-cta.php
/**
* Title: Hero with CTA
* Slug: themescorners/hero-with-cta
* Categories: featured
* Block Types: core/post-content
*/
?>
<!-- wp:cover {"url":"..."} -->
...
<!-- /wp:cover -->
The file is automatically registered, translatable, and visible in the inserter. No PHP hook required.
Best practice #4 — version your patterns
When you change a pattern, every page that inserted it keeps the old serialised block markup. To migrate cleanly, treat patterns like database migrations:
- Ship the new pattern under a new slug (
hero-v2). - Add a
block.jsonmigrationto remap old block names if the structure changed. - Hide the old slug from the inserter with the
block_pattern_categoriesfilter once you're ready. - Don't delete the old pattern file for at least one major release — pages that still reference it will silently lose markup if you do.
Best practice #5 — use templateLock on patterns meant for editors
If a pattern represents a fixed layout (e.g. a 3-column features section), set "templateLock":"all" in the wrapping group. Editors will be able to change the text and images, but not break the layout. Massive support-ticket reducer.
What's coming next
WordPress 6.9 (currently in beta) adds typed binding sources — your callback declares the data type (string, image, url) and the editor uses the right input UI automatically. If you're shipping bindings today, structure your sources so the migration to 6.9 is a single key per source. The exact field is "type", sitting next to "label".
Further reading
- The Block Bindings API handbook is the canonical reference (we link to the WordPress.org page from our docs).
- Our own Free vs Pro Themes post explains which of our themes already ship binding sources you can extend.