InnerBlocks
InnerBlocks allows blocks to contain other blocks as children, creating nested block structures.
Basic Usage
import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';
export default function Edit() {
return (
<div { ...useBlockProps() }>
<InnerBlocks />
</div>
);
}
export function Save() {
return (
<div { ...useBlockProps.save() }>
<InnerBlocks.Content />
</div>
);
}
InnerBlocks Props
allowedBlocks
Restrict which blocks can be inserted:
const ALLOWED_BLOCKS = [ 'core/paragraph', 'core/heading', 'core/image' ];
export default function Edit() {
return (
<div { ...useBlockProps() }>
<InnerBlocks allowedBlocks={ ALLOWED_BLOCKS } />
</div>
);
}
In block.json:
{
"allowedBlocks": [ "core/paragraph", "core/heading", "core/image" ]
}
template
Define default block structure:
const TEMPLATE = [
[ 'core/heading', { level: 2, placeholder: 'Title' } ],
[ 'core/paragraph', { placeholder: 'Description' } ],
[ 'core/image', {} ],
];
export default function Edit() {
return (
<div { ...useBlockProps() }>
<InnerBlocks template={ TEMPLATE } />
</div>
);
}
Nested templates:
const TEMPLATE = [
[ 'core/columns', {}, [
[ 'core/column', {}, [
[ 'core/image', {} ],
] ],
[ 'core/column', {}, [
[ 'core/heading', { level: 3 } ],
[ 'core/paragraph', {} ],
] ],
] ],
];
templateLock
Control template modifications:
// No new blocks, can't remove/reorder
<InnerBlocks templateLock="all" />
// Can reorder but can't add/remove
<InnerBlocks templateLock="insert" />
// Can't reorder but can add/remove (WordPress 6.3+)
<InnerBlocks templateLock="contentOnly" />
// No restrictions (default)
<InnerBlocks templateLock={ false } />
| Value | Add/Remove | Reorder | Edit Content |
|---|---|---|---|
false |
✓ | ✓ | ✓ |
"all" |
✗ | ✗ | ✓ |
"insert" |
✗ | ✓ | ✓ |
"contentOnly" |
✓ | ✗ | ✓ |
orientation
Set insertion orientation hint:
// Horizontal layout (row)
<InnerBlocks orientation="horizontal" />
// Vertical layout (column) - default
<InnerBlocks orientation="vertical" />
renderAppender
Customize the block appender:
import { InnerBlocks, ButtonBlockAppender } from '@wordpress/block-editor';
// Default appender button
<InnerBlocks renderAppender={ InnerBlocks.DefaultBlockAppender } />
// Button appender
<InnerBlocks renderAppender={ ButtonBlockAppender } />
// No appender
<InnerBlocks renderAppender={ false } />
// Custom appender
<InnerBlocks
renderAppender={ () => (
<button onClick={ /* insert logic */ }>
Add Custom Block
</button>
) }
/>
placeholder
Custom placeholder when empty:
<InnerBlocks
placeholder={
<div className="my-placeholder">
<p>Drag blocks here or click to add</p>
</div>
}
/>
defaultBlock
Default block type when pressing Enter:
<InnerBlocks
defaultBlock={ [ 'core/paragraph', { placeholder: 'Type here...' } ] }
/>
directInsert
Skip inserter modal for single allowed block:
<InnerBlocks
allowedBlocks={ [ 'core/paragraph' ] }
directInsert={ true }
/>
prioritizedInserterBlocks
Prioritize blocks in inserter:
<InnerBlocks
prioritizedInserterBlocks={ [ 'core/paragraph', 'core/image' ] }
/>
useInnerBlocksProps Hook
For more control over the wrapper element:
import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor';
export default function Edit() {
const blockProps = useBlockProps();
const innerBlocksProps = useInnerBlocksProps( blockProps, {
allowedBlocks: [ 'core/paragraph' ],
template: [ [ 'core/paragraph', {} ] ],
} );
return <div { ...innerBlocksProps } />;
}
Separate wrapper and inner content:
export default function Edit() {
const blockProps = useBlockProps();
const { children, ...innerBlocksProps } = useInnerBlocksProps(
{ className: 'inner-content' },
{ allowedBlocks: [ 'core/paragraph' ] }
);
return (
<div { ...blockProps }>
<header>Block Header</header>
<div { ...innerBlocksProps }>
{ children }
</div>
<footer>Block Footer</footer>
</div>
);
}
Save with separate structure:
export function Save() {
const blockProps = useBlockProps.save();
const innerBlocksProps = useInnerBlocksProps.save( {
className: 'inner-content',
} );
return (
<div { ...blockProps }>
<header>Block Header</header>
<div { ...innerBlocksProps } />
<footer>Block Footer</footer>
</div>
);
}
Dynamic Blocks with InnerBlocks
In render.php, inner blocks content is available as $content:
<?php
// render.php
$wrapper_attributes = get_block_wrapper_attributes();
?>
<div <?php echo $wrapper_attributes; ?>>
<header class="block-header">
<?php echo esc_html( $attributes['title'] ); ?>
</header>
<div class="inner-content">
<?php echo $content; // Inner blocks HTML ?>
</div>
<footer class="block-footer">
Footer content
</footer>
</div>
In render_callback:
function render_my_block( $attributes, $content, $block ) {
$wrapper_attributes = get_block_wrapper_attributes();
ob_start();
?>
<div <?php echo $wrapper_attributes; ?>>
<div class="inner-content">
<?php echo $content; ?>
</div>
</div>
<?php
return ob_get_clean();
}
Accessing Inner Blocks Data
In JavaScript (Editor)
import { useSelect } from '@wordpress/data';
import { store as blockEditorStore } from '@wordpress/block-editor';
export default function Edit( { clientId } ) {
const innerBlocks = useSelect(
( select ) => select( blockEditorStore ).getBlocks( clientId ),
[ clientId ]
);
const hasInnerBlocks = innerBlocks.length > 0;
return (
<div { ...useBlockProps() }>
{ ! hasInnerBlocks && <p>Add some blocks!</p> }
<InnerBlocks />
</div>
);
}
In PHP
function render_my_block( $attributes, $content, $block ) {
// Access parsed inner blocks
$inner_blocks = $block->inner_blocks;
foreach ( $inner_blocks as $inner_block ) {
$block_name = $inner_block->name;
$block_attrs = $inner_block->attributes;
$block_html = render_block( $inner_block );
}
return '<div>' . $content . '</div>';
}
Conditional InnerBlocks
Show InnerBlocks based on attributes:
export default function Edit( { attributes } ) {
const { showContent } = attributes;
return (
<div { ...useBlockProps() }>
{ showContent && <InnerBlocks /> }
</div>
);
}
Multiple InnerBlocks Areas
A block can only have one InnerBlocks. For multiple areas, use nested blocks:
// Parent block
const TEMPLATE = [
[ 'my-plugin/header-area', {} ],
[ 'my-plugin/content-area', {} ],
[ 'my-plugin/footer-area', {} ],
];
export default function Edit() {
return (
<div { ...useBlockProps() }>
<InnerBlocks
template={ TEMPLATE }
templateLock="all"
/>
</div>
);
}
// Each area block has its own InnerBlocks
// my-plugin/header-area
export function HeaderEdit() {
return (
<header { ...useBlockProps() }>
<InnerBlocks allowedBlocks={ [ 'core/heading', 'core/image' ] } />
</header>
);
}
Layout Support with InnerBlocks
Enable layout controls for inner blocks:
{
"supports": {
"layout": true
}
}
import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor';
export default function Edit() {
const blockProps = useBlockProps();
const innerBlocksProps = useInnerBlocksProps( blockProps, {
// Layout is automatically handled via supports
} );
return <div { ...innerBlocksProps } />;
}
With default layout:
{
"supports": {
"layout": {
"default": {
"type": "flex",
"flexWrap": "nowrap"
}
}
}
}
Complete Example: Card Block with InnerBlocks
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "my-plugin/card",
"title": "Card",
"category": "design",
"attributes": {
"title": {
"type": "string"
}
},
"supports": {
"color": {
"background": true,
"text": true
},
"spacing": {
"padding": true
}
},
"allowedBlocks": [ "core/paragraph", "core/heading", "core/image", "core/button" ],
"editorScript": "file:./index.js",
"style": "file:./style.css",
"render": "file:./render.php"
}
// edit.js
import { useBlockProps, useInnerBlocksProps, RichText } from '@wordpress/block-editor';
const TEMPLATE = [
[ 'core/paragraph', { placeholder: 'Card content...' } ],
];
export default function Edit( { attributes, setAttributes } ) {
const { title } = attributes;
const blockProps = useBlockProps( { className: 'card-block' } );
const innerBlocksProps = useInnerBlocksProps(
{ className: 'card-content' },
{
template: TEMPLATE,
templateLock: false,
}
);
return (
<div { ...blockProps }>
<RichText
tagName="h3"
className="card-title"
value={ title }
onChange={ ( value ) => setAttributes( { title: value } ) }
placeholder="Card Title"
/>
<div { ...innerBlocksProps } />
</div>
);
}
// save.js
import { useBlockProps, useInnerBlocksProps, RichText } from '@wordpress/block-editor';
export default function Save( { attributes } ) {
const { title } = attributes;
const blockProps = useBlockProps.save( { className: 'card-block' } );
const innerBlocksProps = useInnerBlocksProps.save( {
className: 'card-content',
} );
return (
<div { ...blockProps }>
<RichText.Content
tagName="h3"
className="card-title"
value={ title }
/>
<div { ...innerBlocksProps } />
</div>
);
}
<?php
// render.php
$wrapper_attributes = get_block_wrapper_attributes( [ 'class' => 'card-block' ] );
$title = $attributes['title'] ?? '';
?>
<div <?php echo $wrapper_attributes; ?>>
<?php if ( $title ) : ?>
<h3 class="card-title"><?php echo wp_kses_post( $title ); ?></h3>
<?php endif; ?>
<div class="card-content">
<?php echo $content; ?>
</div>
</div>
Best Practices
-
Always use
InnerBlocks.Contentin Save: The save function must render inner blocks content. -
Use templates for structure: Define templates to guide content creation.
-
Consider template locking: Lock templates when structure is important.
-
Restrict allowed blocks: Limit to relevant blocks for better UX.
-
Use
useInnerBlocksPropsfor control: Provides more flexibility than<InnerBlocks />component. -
Handle empty state: Show placeholder or instructions when no inner blocks exist.
-
Test with various content: Ensure your block handles different combinations of inner blocks.