So far our Callout has fixed title and body RichText fields. What if a user wants to put a list inside? Or an image? InnerBlocks lets the user nest arbitrary blocks.
Refactor: Edit component
Replace body RichText with InnerBlocks:
import { InspectorControls, useBlockProps, RichText, InnerBlocks } from '@wordpress/block-editor';
export default function Edit({ attributes, setAttributes }) {
const { title, calloutType } = attributes;
const blockProps = useBlockProps({
className: `my-callout my-callout-${calloutType}`,
});
return (
<div {...blockProps}>
<RichText
tagName="h4"
value={title}
onChange={(value) => setAttributes({ title: value })}
/>
<InnerBlocks
allowedBlocks={['core/paragraph', 'core/list', 'core/code', 'core/image']}
template={[
['core/paragraph', { placeholder: 'Callout body...' }],
]}
/>
</div>
);
}
Save component
InnerBlocks has its own Content component for save-time:
import { InnerBlocks, useBlockProps, RichText } from '@wordpress/block-editor';
export default function Save({ attributes }) {
const { title, calloutType } = attributes;
const blockProps = useBlockProps.save({
className: `my-callout my-callout-${calloutType}`,
});
return (
<div {...blockProps}>
<RichText.Content tagName="h4" value={title} />
<InnerBlocks.Content />
</div>
);
}
InnerBlocks.Content serializes the nested blocks to HTML at save time.
Key props
allowedBlocks— array of block names users can insert inside. Restricting to relevant blocks keeps the UX clean.template— initial blocks when the block is first inserted. Each item is[blockName, attributes, [...innerBlocks]].templateLock—'all'(no add/remove/move),'insert'(no add/remove),false(default).renderAppender— control the ”+ Add block” button placement.
Common patterns
Card with image + text:
<InnerBlocks
allowedBlocks={['core/image', 'core/heading', 'core/paragraph']}
template={[
['core/image'],
['core/heading', { level: 3, placeholder: 'Card title' }],
['core/paragraph', { placeholder: 'Card body...' }],
]}
templateLock="all"
/>
Two-column layout:
<InnerBlocks
allowedBlocks={['core/column']}
template={[
['core/column', { width: '50%' }],
['core/column', { width: '50%' }],
]}
orientation="horizontal"
/>
When to use InnerBlocks vs RichText
| Use RichText when… | Use InnerBlocks when… |
|---|---|
| Field is a single text fragment | Field could be multiple paragraphs, lists, images |
| Format is constrained (inline only) | User needs full block flexibility |
Block.json source = 'html' works | Content is structurally arbitrary |
What’s next
Part 7 adds visual variations and pre-defined styles to the block — letting users pick “Boxed”, “Plain”, or “Filled” variants without writing CSS.
Get the next one in your inbox. Practical tips, no fluff.
More tutorials
View all-
Block Patterns
Register reusable block compositions — landing-page hero, feature grid, FAQ section — that users insert as a complete unit.
25 min
-
Block Attributes and Serialization
Master the attribute system — types, sources, defaults, and how block data round-trips between the editor, the database, and the front-end.
30 min
-
Building a Block Plugin
Package your block as a proper WordPress plugin — file structure, asset enqueueing, server-side render callbacks, plugin metadata.
35 min