MW
Tutorial

Inner Blocks and Composition

Let users nest other blocks inside yours — InnerBlocks, allowedBlocks, template — to build container blocks like cards, columns, and panels.

intermediate 40 min May 13, 2026
Callout block with inspector controls (Part 5)

INNER BLOCKS AND COMPOSITION

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 fragmentField could be multiple paragraphs, lists, images
Format is constrained (inline only)User needs full block flexibility
Block.json source = 'html' worksContent 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.

You've completed this tutorial!

Get the next one in your inbox. Practical tips, no fluff.

Subscribe

Get weekly notes in your inbox

Practical tips, tutorials and resources. No spam.