What the Block Editor’s InnerBlocks API Reveals About Your Component Boundaries

Every time you drop a Columns block into the editor, you’re making a silent architectural bet. You’re betting that the nested blocks inside will behave, that the markup won’t blow out, and that the editing experience won’t turn into a game of DOM whack-a-mole. The InnerBlocks API is the mechanism that enforces that bet—and it’s also the sharpest diagnostic tool you have for understanding where your component boundaries actually live.

I’m Jonas Venn, and I build WordPress systems where the editor is the product. Not the theme, not the plugins—the editor. When I audit a site’s block architecture, I don’t start with the PHP templates. I start by opening the block editor and asking: What can I nest inside what? The answer almost always reveals whether the system was designed or just assembled.

The InnerBlocks Contract: More Than Just a Slot

InnerBlocks isn’t a container. It’s a contract between a parent block and its children. When you define allowedBlocks, template, or templateLock, you’re not just restricting options—you’re declaring what this block means. A Group block with no restrictions says “I’m a generic wrapper.” A Group block locked to a specific set of inner blocks says “I’m a card component, and here’s my anatomy.”

The API forces you to answer questions that most developers skip. What can live inside this block? What must live inside it? Can the user rearrange things, or is the order part of the design? These aren’t UI questions. They’re boundary questions. And the way you answer them determines whether your blocks compose cleanly or leak implementation details all over the editor canvas.

The Template Lock Spectrum

WordPress gives you three lock states: false (no lock), insert (can’t add or remove blocks, but can reorder), and all (completely locked). Most developers treat this as a simple permission toggle. It’s not. It’s a signal about the block’s role in the system.

A block with templateLock: false is a layout primitive. It’s saying “I’m a zone, not a component.” Think of the core Group block—it’s a div with no opinion. A block with templateLock: 'all' is a finished component. It’s saying “My internals are an implementation detail. Don’t touch them.” The Query Loop block uses this pattern: you can configure it, but you can’t rearrange its guts.

The interesting case is templateLock: 'insert'. This is the component that has a defined structure but allows variation within that structure. A card block might lock the image, title, and button in order, but let you swap the image or change the button label. The boundary is semi-permeable. The parent owns the arrangement; the children own their content.

Developer working on block editor component structure

What InnerBlocks Exposes About Your Architecture

When you define an InnerBlocks area, you’re making a statement about coupling. Tightly coupled blocks know exactly what their children are. Loosely coupled blocks accept anything. The problem is that most real-world components need something in between—and that’s where the API gets interesting.

The Allowed Blocks Trap

Setting allowedBlocks to a long list feels safe. You’re being permissive while still maintaining some control. But you’re actually creating a fragile boundary. Every block you allow is a promise you’re making: “This parent will render correctly no matter which of these blocks you insert.” That’s a heavy promise. A Columns block can handle it because it’s been battle-tested with every core block. Your custom card block probably can’t.

I’ve seen teams spend weeks debugging layout shifts because someone dropped an unexpected block into a custom container. The parent’s CSS assumed a certain DOM structure. The child brought its own margin rules. The result was a visual mess that no amount of editor-side validation could prevent—because the problem wasn’t in the editor. It was in the boundary definition.

The fix isn’t to add more CSS. It’s to tighten the contract. If your block only works with a specific set of children, say so explicitly. A short allowedBlocks list isn’t restrictive—it’s honest.

When Templates Become Specifications

The template property in InnerBlocks is often treated as a convenience: “I want these blocks to appear by default.” But it’s actually a specification. It defines the minimum viable structure of your component. When you combine it with templateLock: 'insert', you’re saying “This structure is non-negotiable, but the content within it is flexible.”

This is where the API reveals whether you’ve thought through your component’s anatomy. A template that’s too rigid—every attribute hardcoded—creates a block that’s just a fancy shortcode. Users can’t make it their own. A template that’s too loose—just a Group with a suggested starting point—doesn’t actually help anyone. The sweet spot is a template that encodes the relationships between inner blocks without dictating their content.

Consider a “Team Member” block. A good template might lock in the structure: avatar, name, role, bio. But it leaves the avatar image, the name text, and the bio content all editable. The boundary is clear: this block represents a person, and it always has these four facets. The user can’t accidentally delete the name field or add a random paragraph above the avatar. The component’s meaning is preserved.

Composition vs. Inheritance: The InnerBlocks Way

WordPress blocks don’t inherit from each other. There’s no “Base Card Block” that you extend to create a “Product Card Block.” Instead, you compose. A Product Card is a Group block with a specific InnerBlocks template. This is a fundamentally different mental model from traditional OOP, and InnerBlocks is the mechanism that makes it work.

This composition model forces you to think in terms of boundaries. What does the parent block own? The layout, the spacing, the overall structure. What do the children own? Their own rendering, their own controls, their own data. The parent sets the grid; the children fill the cells. When this boundary is clear, blocks compose beautifully. When it’s fuzzy, you get blocks that fight each other for control of the markup.

I once reviewed a custom block that used InnerBlocks but also injected wrapper divs around each child via PHP. The developer was trying to enforce consistent styling. But they’d crossed the boundary—the parent was now meddling in the children’s rendering. The result was that core blocks like Paragraph and Image broke in subtle ways because they expected to own their root element. The fix was to move that styling concern into the parent’s CSS and let the children render cleanly.

Abstract representation of component boundaries in block editor

InnerBlocks and the Save Function: Where Boundaries Get Tested

The real stress test for your InnerBlocks boundaries happens in the save function. The editor gives you a controlled environment—you can restrict blocks, lock templates, and validate content. But the save function has to produce markup that works in any theme, with any plugins, under any conditions. If your boundary definitions were sloppy in the editor, they’ll shatter on the front end.

This is why I always validate InnerBlocks content in the save function, even when I’ve locked the template. A locked template prevents users from breaking the structure in the editor, but it doesn’t protect against programmatic changes, import/export edge cases, or future core updates that might alter block markup. The save function is your last line of defense. If you’re not checking that the expected inner blocks are present and in the right order, you’re shipping fragile components.

Dynamic vs. Static Rendering Decisions

The choice between JavaScript saving and PHP rendering often comes down to InnerBlocks complexity. If your block’s children need server-side data—post queries, user metadata, external API calls—you’ll need a dynamic render. But that dynamic render now has to reconstruct the parent-child relationships that InnerBlocks managed in the editor. This is where the boundary gets tested again.

A dynamic block that uses InnerBlocks has to parse the serialized block content, extract the inner blocks, and render them in the correct context. If you’ve been loose with your allowed blocks, you now have to handle arbitrary children in PHP. That’s a much harder problem than handling them in JavaScript. The editor’s block validation will catch some issues, but it won’t catch everything—especially if you’re rendering in a different context than the editor preview.

The lesson: if you’re using dynamic rendering, be even more strict with your InnerBlocks definitions. The PHP side has less guardrail infrastructure than the editor side. Your boundaries need to be explicit enough that the PHP render function can make safe assumptions about what children it will receive.

Practical Patterns for Clean InnerBlocks Boundaries

After building and auditing dozens of block-based systems, I’ve settled on a few patterns that consistently produce clean boundaries. These aren’t rules—they’re heuristics that help me decide how to structure a block before I write any code.

Pattern 1: The Container

Use when the block’s only job is to group other blocks. No template lock, no allowed blocks restrictions, minimal styling. The core Group block is the canonical example. Your custom container blocks should follow the same pattern unless you have a very specific reason not to. Containers are the most reusable blocks in your system—don’t pollute them with opinions.

Pattern 2: The Compound Component

Use when the block represents a specific UI pattern that requires a fixed set of sub-components. Lock the template to insert or all, define a strict template, and restrict allowed blocks to only those that make sense in the slots. A “Hero” block might lock in a heading, paragraph, and button group. Users can edit the content but can’t break the hero structure.

Pattern 3: The Collection

Use when the block manages a list of similar items. The Query Loop is the core example. Lock the template to insert so users can’t add random blocks between items, but allow them to customize the item template. The parent block owns the collection logic; the template defines the item boundary.

Each pattern makes a different tradeoff between flexibility and structural integrity. The key is to choose consciously. Don’t default to the most permissive settings just because they’re easier to implement. The editor experience will pay the price later.

WordPress block editor interface showing nested block structure

When InnerBlocks Reveals Deeper System Problems

Sometimes the issue isn’t with a single block’s InnerBlocks configuration. It’s with the entire block system. I’ve seen sites where every custom block uses InnerBlocks with no restrictions, and the result is a content editing experience that’s indistinguishable from raw HTML—but with more clicks. The blocks don’t compose; they just accumulate.

This happens when teams build blocks in isolation. Each developer makes their block as flexible as possible because they don’t know how it will be used. But flexibility without guidance is just complexity. The editor becomes a blank canvas with too many brush types and no instructions on which ones work together.

The fix is to think in terms of a block system, not a block library. Define a small set of container blocks with clear composition rules. Define a set of leaf blocks that are designed to live inside those containers. Then use InnerBlocks restrictions to enforce those relationships. The result is an editor that guides users toward coherent layouts instead of leaving them to figure it out through trial and error.

This is where the real architectural work happens. It’s not about individual block code. It’s about the contracts between blocks. InnerBlocks is the API surface where those contracts are expressed. If you’re not using it intentionally, you’re not designing a block system—you’re just accumulating blocks.

Testing Your Boundaries Before You Write Code

Here’s a practice I’ve adopted that saves weeks of refactoring: before writing a single line of block code, I sketch the InnerBlocks structure on paper. I draw the parent block as a box. I draw the allowed children as smaller boxes inside. I draw arrows showing what can be moved, deleted, or added. Then I ask three questions:

What happens if a user deletes a child block that the parent’s CSS depends on? If the answer is “the layout breaks,” you need a stricter template or a fallback render.

What happens if a user inserts a block type you didn’t anticipate? If the answer is “it might work, it might not,” you need an explicit allowed blocks list.

What happens when this block is nested inside another block that also uses InnerBlocks? If the answer is “I don’t know,” you need to test it. Deeply nested InnerBlocks can create editor performance issues and confusing UX if the boundaries aren’t clear.

These three questions catch 80% of the problems I see in production block systems. The remaining 20% are usually edge cases around block recovery, deprecation, and cross-version compatibility—but those are topics for another article.

FAQ

When should I use templateLock: ‘all’ vs. templateLock: ‘insert’?

Use templateLock: 'all' when the block’s internal structure is an implementation detail that users should never modify. This is appropriate for highly specific components like a “Pricing Table” where the columns, features, and CTAs are all part of a fixed design. Use templateLock: 'insert' when the structure matters but the content within each slot should be editable. A “Testimonial” block might always need an avatar, quote, and attribution in that order, but the user should be able to change the quote text and swap the avatar image.

How do I handle InnerBlocks when my block needs server-side rendering?

When using dynamic rendering with InnerBlocks, parse the serialized block content in your PHP render callback using parse_blocks(). Loop through the inner blocks and render each one with render_block(). Be explicit about which blocks you expect—if a user somehow inserted an unsupported block, either skip it or render a fallback. Don’t assume the editor’s restrictions will always hold. Serialized content can come from anywhere.

Can I nest a block with its own InnerBlocks inside another block’s InnerBlocks?

Yes, and this is where composition shines. A “Section” block can contain “Card” blocks, and each Card can have its own InnerBlocks with a locked template. The key is to keep each level’s boundaries clear. The Section owns the layout of Cards. Each Card owns its internal structure. Don’t let the Section’s CSS reach into the Card’s internals, and don’t let the Card assume anything about its parent container. This separation is what makes the pattern reusable.

What’s the relationship between InnerBlocks boundaries and theme compatibility?

Your InnerBlocks definitions don’t directly affect theme compatibility, but the markup your save function produces does. If you’re locking templates and restricting allowed blocks, you can make stronger assumptions about your output markup—which means you can write more specific CSS. But that CSS needs to work with the theme’s global styles. The boundary between your block’s styles and the theme’s styles is another contract you need to manage. For more on debugging when things go wrong, see What to Fix First When a New WordPress Site Says Nothing Found—many of the same diagnostic principles apply to block rendering issues.

The InnerBlocks API is the closest thing WordPress has to a formal component model. It’s not perfect—there are edge cases, performance considerations with deeply nested structures, and limitations in how much you can customize the editing UX. But it’s what we have. And used well, it produces block systems that are predictable, composable, and maintainable. Used poorly, it produces block systems that are just elaborate shortcode generators.

The difference is in how you think about boundaries. Every allowedBlocks array, every templateLock setting, every template definition is a decision about what this block owns and what it shares. Make those decisions explicitly. Your future self—and anyone who edits content on your site—will feel the difference.