InnerBlocks is the Gutenberg component that transforms a custom block into a container that can hold other blocks — enabling layout patterns like cards, hero sections, and tab panels where both the outer structure and the inner content are user-editable. A block using InnerBlocks defines an allowedBlocks prop to restrict which block types can be placed inside it, a template prop to pre-populate the inner area with a specific block arrangement on first insertion, and a templateLock prop to prevent structural changes while still allowing content editing. The template prop accepts an array of block descriptors — each is a tuple of [blockName, attributes, innerBlocks] — making it possible to scaffold a card with a pre-placed heading, paragraph, and button that the editor can modify but not reorder when templateLock="all". On the save side, the block’s save() function renders <InnerBlocks.Content /> in place of the inner blocks, which serializes all child blocks into the post content. For dynamic blocks (server-side rendered), save() returns null and the PHP render callback uses $block->inner_blocks to access the serialized child content, or simply echoes the pre-rendered $content parameter. Block variations registered with registerBlockVariation() provide pre-configured templates under a single block type — a “Two Column Card” variation and a “Hero Section” variation can share the same InnerBlocks block type with different default templates, reducing the number of registered block types. The useInnerBlocksProps() hook merges the InnerBlocks props with the block wrapper props, allowing the inner blocks area to be the same DOM element as the block wrapper — eliminating a superfluous wrapper div in the frontend markup. The REST API post explains the server-side rendering pattern that pairs with dynamic InnerBlocks blocks.
Problem: A theme needs a reusable "Feature Card" section in the block editor — a structured container with a fixed layout (icon, heading, description, button) where editors can change content but not restructure the layout or accidentally delete required inner blocks.
Solution: Register a custom block that renders InnerBlocks with a locked template defining the required child blocks, use allowedBlocks to restrict additions, and output <InnerBlocks.Content /> in save() so the child block markup is serialized into post content.
// feature-card/index.js
import { registerBlockType } from '@wordpress/blocks';
import { InnerBlocks, useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor';
const TEMPLATE = [
[ 'core/image', { className: 'feature-card__icon', sizeSlug: 'thumbnail' } ],
[ 'core/heading', { level: 3, placeholder: 'Feature title' } ],
[ 'core/paragraph', { placeholder: 'Describe this feature…' } ],
[ 'core/buttons', {}, [
[ 'core/button', { text: 'Learn more', className: 'feature-card__cta' } ],
]],
];
const ALLOWED = [ 'core/image', 'core/heading', 'core/paragraph', 'core/buttons' ];
registerBlockType( 'myplugin/feature-card', {
title: 'Feature Card',
category: 'design',
icon: 'star-filled',
attributes: {},
edit( { attributes } ) {
const blockProps = useBlockProps( { className: 'feature-card' } );
const innerBlocksProps = useInnerBlocksProps( blockProps, {
template: TEMPLATE,
templateLock: 'all', // editors can change content, not structure
allowedBlocks: ALLOWED,
} );
return ;
},
save() {
const blockProps = useBlockProps.save( { className: 'feature-card' } );
return (
);
},
} );
// feature-card/block.json
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "myplugin/feature-card",
"title": "Feature Card",
"category": "design",
"editorScript": "file:./index.js",
"style": "file:./style.css",
"editorStyle": "file:./editor.css",
"supports": {
"html": false,
"align": [ "wide", "full" ]
}
}
// Register in PHP
add_action( 'init', function() {
register_block_type( __DIR__ . '/feature-card/block.json' );
} );
NOTE: templateLock="all" prevents adding, removing, or reordering blocks but still allows editing content inside them. Use templateLock="insert" to allow reordering but block additions/removals, or omit it entirely to allow free editing. Choose the lock level based on how strictly the design system needs to enforce layout consistency — over-locking frustrates editors who need flexibility for content that varies in length or structure.