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- WordPress
How to Prepare Your Plugin or Theme for WordPress 7.0
A step-by-step compatibility process for shipping a WordPress 7.0-ready release — set up a test site, audit your blocks, fix the breaking changes, and ship with confidence.
22 min
- WordPress
WordPress 7.0 for Block Developers: Breaking Changes
WordPress 7.0 enforces the iframed editor, broadens contentOnly mode, and drops PHP 7.3. Here are the breaking changes block and plugin developers must fix before users upgrade.
25 min
- WordPress
WordPress 7.0 AI: Client, Abilities & Connectors
The headline feature of WordPress 7.0 is native AI in Core. Here is what the WP AI Client, the Abilities API, and the Connectors system actually mean for plugin developers.
25 min