eddev
Blocks

Nested Blocks

Blocks within blocks

Nested blocks let a block contain other blocks. Use them for layouts like sections, cards, accordions, button rows, reusable rich-text areas, and template-part slots.

The <InnerBlocks /> component is the child block container.

import { defineBlock, EditableText, InnerBlocks } from "eddev/blocks"

export const meta: BlockMeta = {
  title: "Section",
  tags: ["#root"],
}

export default defineBlock("content/section", () => {
  return (
    <section className="grid-auto">
      <EditableText as="h2" store="title" defaultValue="Enter a title" />
      <InnerBlocks allowedBlocks={["#inline"]} />
    </section>
  )
})

You can only have one InnerBlocks in a block. This is a WordPress/Gutenberg limitation. If a block needs multiple editable zones, see SlotBlocks below.

Allowed Blocks

Use allowedBlocks to control what can be inserted inside the container.

<InnerBlocks allowedBlocks={["core/paragraph", "core/list", "content/button-row"]} />

Entries can be specific block names or tags. Tags are usually cleaner when several blocks should be allowed in the same context.

export const meta: BlockMeta = {
  title: "Button Row",
  tags: ["#inline"],
}

export default defineBlock("content/button-row", () => {
  return <InnerBlocks allowedBlocks={["#buttons"]} />
})
export const meta: BlockMeta = {
  title: "Button",
  tags: ["#buttons"],
}

WordPress core blocks can also be tagged and allowed this way. See Core Blocks for the PHP-side setup.

The unprefixed root tag is the framework default, but our examples use explicit #root and set rootBlocks in blocks/_editor.tsx.

Templates And Locking

InnerBlocks can insert default child blocks and control how much authors can change the list.

<InnerBlocks
  allowedBlocks={["content/card-grid-item"]}
  defaultBlocks={[["content/card-grid-item", {}]]}
  templateLock="insert"
/>
  • defaultBlocks are inserted when the container is empty.
  • headerTemplate and footerTemplate keep blocks pinned to the start or end.
  • template defines the whole child block list.
  • templateLock="all" prevents moving, inserting, or deleting.
  • templateLock="insert" prevents inserting or removing, but allows moving existing blocks.
  • templateLock="contentOnly" locks structure but allows rich text editing.
  • templateLock="none" or false leaves the list editable.

Use these sparingly. A good rule is to lock structural wrapper blocks, but keep content blocks flexible.

Appenders And Editor Classes

adminClassName adds a class to the editor-only wrapper. appender changes the inserter UI.

<InnerBlocks
  allowedBlocks={["content/card-grid-item"]}
  adminClassName="grid grid-cols-3 gap-4"
  appender={{ type: "simple" }}
/>

Supported appender types are default, button, button2, simple, or a custom appender created with createAppender.

Rendering Children Yourself

Most blocks can render <InnerBlocks /> directly. If the parent needs to inspect child blocks, use useInnerBlocks().

import { defineBlock, InnerBlocks, useInnerBlocks } from "eddev/blocks"

export default defineBlock("content/card-grid", () => {
  const blocks = useInnerBlocks()
  const columns = blocks.length === 3 ? "grid-cols-3" : "grid-cols-2"

  return (
    <div className={columns}>
      <InnerBlocks allowedBlocks={["content/card-grid-item"]} />
    </div>
  )
})

useInnerBlocks() returns block data with metadata added, including slug, tags, and flags.

Multiple Editable Zones

Use SlotBlocks only when a block genuinely needs multiple independent editable zones.

import { SlotBlocks } from "eddev/blocks"

<SlotBlocks id="primary" allowedBlocks={["#menu"]} />
<SlotBlocks id="cta" template={[["content/button", {}]]} templateLock="insert" />

SlotBlocks stores hidden core/slot-group blocks inside the parent and renders each slot separately. It is useful for complex headers, mega menus, or app-like layouts, but it is more complex than a normal InnerBlocks container.

On this page