Two tools handle repeatable layouts in the WordPress block editor, and they’re often confused because they both involve groups of blocks: Block Patterns and InnerBlocks. They’re not alternatives — they solve different problems, and knowing which one fits your situation is the difference between an editor experience that feels natural and one that constantly fights you.
Block Patterns are for editors. InnerBlocks are for developers. Once that distinction is clear, the right choice in any given situation becomes obvious.
What Block Patterns Are
A Block Pattern is a predefined arrangement of blocks that editors can insert into any post or page with one click. A pricing table, a hero section, a two-column feature comparison, a testimonial grid — any layout you build from standard WordPress blocks can be saved as a pattern and made available in the block inserter.
The key property of a pattern: after insertion, each copy is independent. An editor inserts a pattern, edits the text in that instance, and the original pattern and every other inserted copy are unaffected. Patterns are templates, not live links.
This is distinct from Synced Patterns (called Reusable Blocks before WordPress 6.3). Synced Patterns do maintain a live link — editing one instance updates every instance sitewide. Use a Synced Pattern for content that must be identical everywhere: a disclaimer paragraph, a contact CTA block, a promotional banner. Use a regular (unsynced) pattern for layout templates where each instance gets unique content.
Registering Block Patterns in PHP
The standard way to register a pattern for a plugin or classic theme is register_block_pattern(), called on the init hook:
function my_theme_register_patterns() {
register_block_pattern(
'my-theme/hero-section',
array(
'title' => __( 'Hero Section', 'my-theme' ),
'description' => __( 'A full-width hero with heading, description, and CTA button.', 'my-theme' ),
'categories' => array( 'my-theme-layouts' ),
'keywords' => array( 'hero', 'banner', 'header' ),
'content' => '<!-- wp:group {"align":"full","style":{"spacing":{"padding":{"top":"6rem","bottom":"6rem"}}}} -->
<div class="wp-block-group alignfull">
<!-- wp:heading {"level":1,"placeholder":"Hero headline"} -->
<h1 class="wp-block-heading">Your Headline Here</h1>
<!-- /wp:heading -->
<!-- wp:paragraph {"placeholder":"Supporting text"} -->
<p>A short sentence that supports the headline and explains what this page is about.</p>
<!-- /wp:paragraph -->
<!-- wp:buttons -->
<div class="wp-block-buttons">
<!-- wp:button -->
<div class="wp-block-button"><a class="wp-block-button__link wp-element-button">Get started</a></div>
<!-- /wp:button -->
</div>
<!-- /wp:buttons -->
</div>
<!-- /wp:group -->',
)
);
}
add_action( 'init', 'my_theme_register_patterns' );
The content field is raw block comment markup — the same format WordPress saves to post_content. The easiest way to get this markup is to build your layout in the editor, then switch to the Code Editor view and copy the block markup directly.
To group patterns logically in the inserter, register a pattern category first:
register_block_pattern_category(
'my-theme-layouts',
array( 'label' => __( 'My Theme Layouts', 'my-theme' ) )
);
Theme Patterns in Block Themes (FSE)
In a block theme built for Full Site Editing, the recommended way to ship patterns is through the /patterns/ directory — no PHP registration needed. Each .php file in this directory is automatically registered as a pattern, using a metadata header at the top of the file:
<?php
/**
* Title: Hero Section
* Slug: my-theme/hero-section
* Categories: featured, my-theme-layouts
* Keywords: hero, banner
* Block Types: core/template-part/header
* Inserter: true
*/
?>
<!-- wp:group {"align":"full"} -->
<div class="wp-block-group alignfull">
<!-- wp:heading {"level":1} -->
<h1 class="wp-block-heading"><?php echo esc_html__( 'Your headline here', 'my-theme' ); ?></h1>
<!-- /wp:heading -->
</div>
<!-- /wp:group -->
The Block Types header is particularly useful: it limits the pattern’s availability to specific block types, so a pattern designed for the header template part only appears when you’re editing that template part — not in every post inserter. This approach keeps the block theme’s pattern library organised and context-aware.
For building the full block theme structure that these patterns live inside, the guide to high-performance block themes in WordPress 6.5+ covers the complete directory structure.
What InnerBlocks Are
InnerBlocks is a React component used inside a custom block’s edit.js that creates a nested block container — a slot where editors can add other blocks. It’s how you build a custom “Card” block that lets editors put an image, heading, and paragraph inside; or a “Tabs” block where each tab contains whatever blocks the editor chooses.
The core difference from patterns: InnerBlocks gives you programmatic control over structure. You define which blocks are allowed, what the default structure is, and whether the structure can be changed at all — and those constraints are enforced by the editor UI, not just by convention.
InnerBlocks in a Custom Block
A minimal custom block using InnerBlocks has three pieces:
edit.js — the editor component:
import { InnerBlocks } from '@wordpress/block-editor';
const TEMPLATE = [
[ 'core/image', { placeholder: 'Card image' } ],
[ 'core/heading', { level: 3, placeholder: 'Card title' } ],
[ 'core/paragraph', { placeholder: 'Card description...' } ],
];
export default function Edit() {
return (
<div { ...useBlockProps() }>
<InnerBlocks
template={ TEMPLATE }
allowedBlocks={ [ 'core/image', 'core/heading', 'core/paragraph', 'core/button' ] }
templateLock="insert"
/>
</div>
);
}
save.js — the front-end output:
import { InnerBlocks } from '@wordpress/block-editor';
export default function save() {
return (
<div { ...useBlockProps.save() }>
<InnerBlocks.Content />
</div>
);
}
templateLock controls what editors can do with the template:
"all"— the template is completely locked: editors can edit the content of each block but cannot add, remove, or reorder blocks"insert"— editors can reorder existing blocks but cannot add or remove themfalse— no lock: the template provides defaults but editors have full freedom
useInnerBlocksProps: The Modern Approach
For blocks where the InnerBlocks container needs custom HTML attributes or CSS classes — or where the block wrapper and the InnerBlocks container need to be the same element — use the useInnerBlocksProps hook instead of the <InnerBlocks /> component:
import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor';
export default function Edit() {
const blockProps = useBlockProps( { className: 'my-card' } );
const innerBlocksProps = useInnerBlocksProps( blockProps, {
template: TEMPLATE,
allowedBlocks: [ 'core/image', 'core/heading', 'core/paragraph' ],
templateLock: 'insert',
orientation: 'vertical',
} );
return <div { ...innerBlocksProps } />;
}
The difference: with <InnerBlocks />, the block wrapper and the InnerBlocks container are separate elements (an extra wrapper div). With useInnerBlocksProps, they merge — the block wrapper IS the InnerBlocks container. This produces cleaner HTML on the front end by eliminating one layer of nesting.
The orientation option controls how keyboard navigation works between blocks in the editor and how the built-in appender appears: "horizontal" for flex-row layouts (like a button group), "vertical" (the default) for stacked layouts.
Block Patterns vs. InnerBlocks: When to Use Which
| Situation | Use | Reason |
|---|---|---|
| Predefined layout editors insert and customise freely | Block Pattern | No custom block needed; pattern is decoupled on insertion |
| Content that must be identical across every instance | Synced Pattern | Live link between all instances; one edit updates all |
| Custom block that contains editor-chosen sub-blocks | InnerBlocks | Creates a nested block container inside the parent block |
| Block with a fixed structure editors can’t break | InnerBlocks + templateLock: "all" |
Enforces the layout at the editor level, not just by convention |
| Complex functional block (tabs, accordion, slider) | InnerBlocks + Interactivity API | InnerBlocks handles layout; Interactivity API handles behaviour |
| FSE theme layout templates | Theme pattern in /patterns/ |
Auto-registered by block theme, context-aware via Block Types header |
| Pattern shared between a plugin and the pattern directory | register_block_pattern() |
PHP registration gives full control over availability and metadata |
The most common mistake is reaching for a custom block with InnerBlocks when a Block Pattern would do. If editors just need a layout starting point they can freely modify — and you don’t need to programmatically control what goes inside — a pattern is significantly less code and has zero JavaScript overhead.
Combining Patterns and InnerBlocks
The two tools compose cleanly. A Block Pattern can include custom blocks that use InnerBlocks — the pattern provides the outer layout template (which editors can customise freely after insertion), while the InnerBlocks-based blocks inside it enforce structural constraints on their own content.
For example: a “Team Section” pattern might include three instances of a custom “Team Member Card” block. Each card uses InnerBlocks with templateLock: "insert" to ensure every card always has an image, a name heading, and a title paragraph — but the pattern itself is unsynced, so editors can adjust the section layout and card content freely per page.
For complex blocks that combine InnerBlocks with dynamic front-end behaviour (tabs where clicking a tab shows the right InnerBlocks panel, for example), the WordPress Interactivity API handles the JavaScript layer — using data-wp-on--click and context state rather than jQuery to toggle visibility between InnerBlocks panels.
Patterns in Headless WordPress
When WordPress is used headless — with a Next.js frontend consuming the REST API — Block Patterns are stored in post_content as block comment markup, which the REST API returns as raw HTML in the content.rendered field. The inserted pattern becomes part of the post content, no different from any other block. This means patterns require no special handling in a headless setup — they render as part of the standard content field.
For custom blocks using InnerBlocks in a headless context, the rendered HTML of the nested blocks is also included in content.rendered. Real-time content synchronisation between WordPress and a decoupled frontend can be handled via WordPress webhooks and Next.js revalidation, which triggers a rebuild of the affected pages whenever post content changes.
Performance Characteristics
Block Patterns have zero front-end overhead — once inserted, they’re standard block markup stored in post_content. There’s no runtime pattern code; the editor just uses the markup as a starting point. The rendered HTML is no different from blocks placed individually.
Custom blocks using InnerBlocks do require a small JavaScript bundle (the block’s editorScript) — but only in the editor. On the front end, save.js with <InnerBlocks.Content /> renders as static HTML with no runtime dependency. The front-end output of an InnerBlocks block is identical in performance to manually placed blocks; the JavaScript cost is editor-only.
For a complete guide to custom Gutenberg block development — registering block.json, writing edit.js and save.js, and using useBlockProps — the custom Gutenberg block guide covers the full workflow. If you’re building blocks as part of a bespoke theme, the custom theme development service covers the full scope — block registration, pattern library, FSE template structure, and Interactivity API integration.


