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.1
A WordPress 7.1 compatibility checklist: iframed editor, client-side media hooks, Abilities public flag, jQuery UI 1.14.2, plus features that did not ship (React 19, collaboration).
22 min
- WordPress
What's New in WordPress 7.1 for Developers
WordPress 7.1 field guide for developers: client-side media, Abilities API, always-iframed editor, SVG Icon API, global styles, and what to test before users upgrade.
18 min
- WordPress
Abilities API Changes in WordPress 7.1
WordPress 7.1 upgrades the Abilities API: a unified public flag, wp_get_abilities() filtering, execution lifecycle hooks, JSON Schema for clients, and typed REST input.
25 min