Register Custom Block Patterns in WordPress via PHP (2026 Guide)
Block patterns are reusable block compositions that appear in the editor’s pattern picker. You can register them via PHP (register_block_pattern()), via JSON files in /patterns/ of a block theme, or as synced patterns (the old reusable-blocks) via the Site Editor. Each path has trade-offs. This snippet covers all three, explains when to pick which, and ships five production-ready example patterns you can drop in.
I’ve built block patterns three different ways over the last four years, and every time I told myself “this time, I’ll finally pick the right one”. The honest answer is that you need all three — different patterns belong in different places. A client’s brand-specific hero section lives in a theme’s /patterns/ folder because it travels with the theme. A generic “pricing table” I reuse across 15 sites is registered via PHP in a site-agnostic mu-plugin. A synced pattern is right for a marketing team that wants to edit once and propagate to 200 pages. The examples below show each approach at the minimum viable quality — enough detail to ship, enough cross-references to understand the trade-offs.
What this snippet covers
- PHP registration —
register_block_pattern()with title, description, category, keywords, block-types context, and the block markup itself - JSON / theme-file registration — the modern block-theme approach where patterns live as
.phpfiles in/patterns/and auto-register - Synced patterns (ex-reusable-blocks) — when and how to use, how they differ from the other two
- Pattern categories — registering your own categories so your patterns group cleanly in the pattern picker
- 5 production-ready example patterns — hero, pricing, testimonials, FAQ, call-to-action — using core blocks only so they work on any theme
- Contextual patterns — patterns scoped to specific post types via the
postTypesparameter (useful for CPTs) - Pattern-only categories in the picker — hiding the default “Featured” / “About” clutter if you only want your branded patterns visible
- WP-CLI commands to list registered patterns and debug which are actually loading
Install and use
Drop the mu-plugin block below into wp-content/mu-plugins/gt-patterns.php. For the theme-file approach, create a /patterns/ folder in your active theme and save each pattern as a separate .php file — WordPress 6.0+ auto-registers every file in that folder. For synced patterns, no code needed — create them in the Site Editor under Patterns → Manage my patterns.
<?php
/**
* Plugin Name: GT Custom Block Patterns
* Description: PHP-registered patterns + custom categories. Pair with theme /patterns/ files.
*/
defined( 'ABSPATH' ) || exit;
/* ============================================================
* 1. Register a custom pattern category first
* (patterns registered against a non-existent category silently land in "Uncategorized")
* ============================================================ */
add_action( 'init', function () {
register_block_pattern_category( 'gt-brand', [
'label' => __( 'Gaurav Tiwari' ),
] );
register_block_pattern_category( 'gt-ctas', [
'label' => __( 'Call to Action' ),
] );
} );
/* ============================================================
* 2. Register a PHP-defined pattern (simple, always works)
* ============================================================ */
add_action( 'init', function () {
if ( ! function_exists( 'register_block_pattern' ) ) return;
/* Hero with headline + subhead + two buttons */
register_block_pattern( 'gt/hero-simple', [
'title' => __( 'Hero — Headline + Two CTAs' ),
'description' => __( 'Centered hero section with a bold headline, supporting line, and two call-to-action buttons.' ),
'categories' => [ 'gt-brand' ],
'keywords' => [ 'hero', 'landing', 'cta', 'headline' ],
'content' => '<!-- wp:group {"align":"full","layout":{"type":"constrained"}} -->
<div class="wp-block-group alignfull" style="padding:5rem 1rem;text-align:center">
<!-- wp:heading {"level":1,"style":{"typography":{"fontSize":"3rem","lineHeight":"1.1"}}} -->
<h1 style="font-size:3rem;line-height:1.1">Build better WordPress sites</h1>
<!-- /wp:heading -->
<!-- wp:paragraph -->
<p>Production-ready snippets and tooling from 16+ years of practice.</p>
<!-- /wp:paragraph -->
<!-- wp:buttons {"layout":{"type":"flex","justifyContent":"center"}} -->
<div class="wp-block-buttons">
<!-- wp:button --><div class="wp-block-button"><a class="wp-block-button__link" href="/code/">Browse snippets</a></div><!-- /wp:button -->
<!-- wp:button {"className":"is-style-outline"} --><div class="wp-block-button is-style-outline"><a class="wp-block-button__link" href="/about/">About me</a></div><!-- /wp:button -->
</div>
<!-- /wp:buttons -->
</div>
<!-- /wp:group -->',
] );
/* Pricing table — 3 columns */
register_block_pattern( 'gt/pricing-3col', [
'title' => __( 'Pricing — 3 Tiers' ),
'description' => __( 'Three-column pricing table with tier labels, prices, feature lists, and CTAs.' ),
'categories' => [ 'gt-brand' ],
'keywords' => [ 'pricing', 'tiers', 'plans' ],
'content' => '<!-- wp:columns --><div class="wp-block-columns">
<!-- wp:column --><div class="wp-block-column">
<!-- wp:heading {"level":3} --><h3>Starter</h3><!-- /wp:heading -->
<!-- wp:heading {"level":2} --><h2>$9/mo</h2><!-- /wp:heading -->
<!-- wp:list --><ul><li>Feature one</li><li>Feature two</li></ul><!-- /wp:list -->
</div><!-- /wp:column -->
<!-- wp:column --><div class="wp-block-column">
<!-- wp:heading {"level":3} --><h3>Pro</h3><!-- /wp:heading -->
<!-- wp:heading {"level":2} --><h2>$29/mo</h2><!-- /wp:heading -->
<!-- wp:list --><ul><li>Everything in Starter</li><li>Feature three</li></ul><!-- /wp:list -->
</div><!-- /wp:column -->
<!-- wp:column --><div class="wp-block-column">
<!-- wp:heading {"level":3} --><h3>Team</h3><!-- /wp:heading -->
<!-- wp:heading {"level":2} --><h2>$99/mo</h2><!-- /wp:heading -->
<!-- wp:list --><ul><li>Everything in Pro</li><li>5 seats</li></ul><!-- /wp:list -->
</div><!-- /wp:column -->
</div><!-- /wp:columns -->',
] );
/* CTA band scoped to the "post" post type only */
register_block_pattern( 'gt/cta-newsletter', [
'title' => __( 'CTA — Newsletter Signup' ),
'description' => __( 'Full-width newsletter signup block. Use at the end of blog posts.' ),
'categories' => [ 'gt-ctas' ],
'keywords' => [ 'cta', 'newsletter', 'email', 'signup' ],
'postTypes' => [ 'post' ], /* Only appears in pattern picker when editing a post */
'content' => '<!-- wp:group {"align":"full"} --><div class="wp-block-group alignfull" style="padding:3rem 1rem;background:#f4f4f4;text-align:center">
<!-- wp:heading {"level":3} --><h3>Want more snippets?</h3><!-- /wp:heading -->
<!-- wp:paragraph --><p>Weekly email. 800 WordPress developers read it. No spam.</p><!-- /wp:paragraph -->
<!-- wp:buttons --><div class="wp-block-buttons"><!-- wp:button --><div class="wp-block-button"><a class="wp-block-button__link" href="/newsletter/">Subscribe</a></div><!-- /wp:button --></div><!-- /wp:buttons -->
</div><!-- /wp:group -->',
] );
} );
/* ============================================================
* 3. Optional — remove WP's default patterns to keep the picker clean
* Comment out if you want core patterns too
* ============================================================ */
add_filter( 'should_load_remote_block_patterns', '__return_false' );
/* ============================================================
* 4. Optional — WP-CLI helper to list patterns
* wp gt-patterns
* ============================================================ */
if ( defined( 'WP_CLI' ) && WP_CLI ) {
WP_CLI::add_command( 'gt-patterns', function () {
$registry = WP_Block_Patterns_Registry::get_instance();
foreach ( $registry->get_all_registered() as $p ) {
WP_CLI::log( sprintf( '%-40s %s', $p['name'], $p['title'] ) );
}
} );
}How the three approaches compare
PHP registration via register_block_pattern() is the most explicit. Patterns are defined in code, versioned in Git, portable across themes. Use when the pattern is site-agnostic and reused across multiple projects — pricing tables, CTA bands, author boxes. Downsides: the content is a single string with escaped newlines, hard to edit, hard to preview until registered. Theme file registration (WordPress 6.0+) uses the /patterns/ folder of a block theme, one .php file per pattern with a standard header block. WordPress auto-discovers and registers them on theme load. Use for theme-specific patterns that should travel with the theme — brand heroes, section layouts unique to the design. Downsides: tied to the active theme; switch themes and the patterns are gone. Synced patterns (formerly reusable blocks) are created by users in the Site Editor. They’re stored as wp_block posts in the database. Changes propagate to every post that uses them. Use for content that multiple pages share and that needs to update in unison — a product announcement banner, a disclaimer, a testimonial you want to edit once and have updated everywhere. Downsides: users can break them by editing; they don’t version-control with code. My rule: brand-specific and pages → theme files. Site-agnostic and reusable → PHP. Content that propagates → synced. All three can coexist; the picker shows them with a small icon distinguishing type.
Download and source
- Full mu-plugin with the 5 patterns + category registration: gist.github.com/wpgaurav — search gt-patterns
- Bundled in the Functionalities plugin
- WordPress core docs: Block Patterns + Block Pattern registration API
- Theme-file auto-registration docs: block-pattern file header reference
- For full FSE pattern management without code: the Site Editor → Patterns panel — create, categorise, mark as synced, all from the UI
FAQs
Which approach should I use for a client project?
Depends on who edits. If the client team doesn’t touch code, PHP patterns are wrong (they need a developer to change). Theme patterns are right for theme-specific brand elements the dev team ships. Synced patterns are right for content the marketing team wants to edit directly. Typical client site uses all three: brand hero as a theme pattern, generic CTAs as synced patterns, and custom listings as PHP patterns.
Can I translate patterns?
Yes, by wrapping the strings in the usual WordPress i18n functions. The title and description go through __() with your text domain. Content strings inside the block markup need esc_html__() or the equivalent, though embedded gettext inside serialised block comments is tricky — most sites don’t translate pattern content, only the title/description.
How do I remove built-in WordPress patterns from the picker?
The should_load_remote_block_patterns filter returning false disables the patterns WordPress.org hosts remotely (that’s the bulk of them). To also remove WP Core’s bundled patterns, use unregister_block_pattern() inside an init callback at priority 99, after they’ve been registered. Target specific names: unregister_block_pattern('core/query-standard-posts'), etc.
What’s the difference between postTypes and templateTypes?
postTypes scopes the pattern to specific post types — only appears in the picker when editing one of the listed types. templateTypes does the same for template parts in FSE (header, footer, 404, etc.). Use either to declutter the picker on contexts where the pattern doesn’t apply.
Can patterns include custom plugin blocks?
Yes, reference them by their full block name in the content — <!-- wp:generateblocks/element -->, <!-- wp:acf/accordion -->, etc. The user needs the plugin active for the pattern to render correctly. If the plugin is inactive, the editor shows a placeholder instead of the custom block.
How do I export a pattern I built in the editor into PHP code?
Open the saved post in the Code Editor view (top-right menu in the Block Editor), select all, copy. Paste into the content key of register_block_pattern(), escape the quotes appropriately, done. Keep it in a single heredoc (<<<'HTML' … HTML) to avoid escape hell.
What if I need per-user or per-role pattern restrictions?
Not supported natively. Use the block restriction snippet to control which blocks each role can use — patterns compose blocks, so restricting blocks indirectly restricts patterns that contain them. For per-role pattern visibility specifically, you’d need to filter the block_editor_settings_all return value.
How do I categorise patterns in the picker?
Register a custom category via register_block_pattern_category(), then reference it in each pattern’s categories array. Multiple categories per pattern are allowed. Shown as sections in the pattern picker sidebar.
Can I use PHP partials inside patterns?
Not directly — pattern content is static HTML plus block comment syntax. To include dynamic data, use dynamic blocks (blocks with a PHP render_callback) inside your patterns. The patterns wrap the blocks; the blocks render the dynamic content.
What about Figma-to-pattern workflows?
Manual or tool-assisted. I use the figma-to-generateblocks converter for GB-specific designs, or copy the rendered HTML from Figma’s Dev Mode and hand-translate to block syntax. No single tool handles arbitrary Figma → WP block conversion well in 2026.