Building a Custom Block Part 8: Restricting Inner Blocks

By Ian Svoboda on February 25, 2026

In Part 8 of Building a Custom Block, we’ll restrict which blocks can be inserted inside our Notice block and use template locking to enforce a consistent inner block structure.

In Part 7, we added contextual styles to the inner blocks inside our Notice block. The heading, paragraph, and links all look like they belong inside a notice now, and the block editor’s built-in controls can still override everything we set.

But there’s a problem we haven’t addressed yet: Right now, a user can insert any block inside the Notice. An image, a table, a shortcode block, a video embed, whatever they want. Our CSS only styles headings, paragraphs, and links. Anything else is going to look out of place and potentially break the visual consistency we’ve worked to establish.

We can solve that by doing two things: restricting which blocks are allowed inside the Notice, and using template locking to give the default inner blocks some structure. These two features work together to create a block that’s flexible enough to be useful but constrained enough to stay visually consistent.

Why Restrict Inner Blocks?

Let’s think about the purpose of a notice:

A notice is a short, focused piece of content meant to draw attention. It’s not a container for a gallery, a video, or a complex layout. The more block types you allow inside, the more edge cases you create for yourself as the block developer.

There are a few practical reasons to restrict inner blocks:

  1. Styling becomes predictable. You only need to write CSS for the blocks you actually support. No surprises.
  2. Content stays focused. A notice with a heading and a paragraph reads clearly. A notice with a heading, two images, a pullquote, and a separator does not.
  3. Maintenance is simpler. Fewer supported inner blocks means fewer things that can look wrong after a theme change or WordPress update.

This doesn’t mean you have to be overly restrictive. The goal is to allow what makes sense and leave out what doesn’t. It’s tempting to say: well people just shouldn’t put in the wrong blocks.

However, I’ve found that in practice, most users will do things if they’re allowed to do them. The assumption there is: well if they didn’t want me to do it, why would they leave that option/capability in?

The idea here is to try and think proactively about removing any footguns that users might experience and have things work well out of the box.

How to set allowedBlocks

WordPress gives you two ways to restrict which blocks can appear inside your block’s InnerBlocks area:

  1. The allowedBlocks field in block.json
  2. The allowedBlocks prop on the useInnerBlocksProps hook (or the InnerBlocks component)

The block.json approach is the simpler option. You define the list once and it applies everywhere, in the editor and on the frontend. This is the right choice when your allowed blocks list is static, meaning it doesn’t change based on attributes or any other dynamic condition.

The JavaScript approach (via useInnerBlocksProps) is useful when you need the list to change dynamically. For example, if a block attribute toggled between “simple” and “advanced” modes and each mode allowed different inner blocks. For most blocks though, a static list in block.json is all you need.

Additionally, the JavaScript approach can be useful if you’re allowing users to filter the allowedBlocks in some way. This is definitely more of an advanced step that would only be valuable to other developers (like yourself).

We’ll use the block.json approach since our allowed blocks won’t change based on anything dynamic.

Adding allowedBlocks to block.json

Open up block.json and add the allowedBlocks field. This is a top-level property that accepts an array of block name strings:

src/blocks/notice/block.json
{
	"$schema": "https://schemas.wp.org/trunk/block.json",
	"apiVersion": 3,
	"name": "lwpd/notice",
	"version": "0.1.0",
	"title": "Notice",
	"category": "widgets",
	"icon": "info-outline",
	"description": "Example block scaffolded with Create Block tool.",
	"example": {},
	"allowedBlocks": [
	  "core/buttons",
		"core/heading",
		"core/paragraph",
		"core/list"
	],
	"attributes": {
		"isDismissible": {
			"type": "boolean",
			"default": false
		},
		"style": {
			"type": "object",
			"default": {
				"spacing": {
					"blockGap": "0.5rem"
				}
			}
		}
	},
	"styles": [
		{
			"name": "info",
			"label": "Info",
			"isDefault": true
		},
		{
			"name": "success",
			"label": "Success"
		},
		{
			"name": "warning",
			"label": "Warning"
		},
		{
			"name": "error",
			"label": "Error"
		}
	],
	"supports": {
		"align": [ "wide", "full" ],
		"anchor": true,
		"color": {
			"background": true,
			"text": true,
			"link": true
		},
		"html": false,
		"layout": {
			"default": {
				"type": "flex",
				"orientation": "vertical"
			}
		},
		"spacing": {
			"margin": true,
			"padding": true,
			"blockGap": true,
			"__experimentalDefaultControls": {
				"blockGap": true,
				"margin": false,
				"padding": false
			}
		},
		"typography": {
			"fontSize": true,
			"lineHeight": true,
			"__experimentalFontFamily": true,
			"__experimentalFontStyle": true,
			"__experimentalFontWeight": true,
			"__experimentalDefaultControls": {
				"fontSize": true,
				"lineHeight": true
			}
		}
	},
	"textdomain": "notice",
	"editorScript": "file:./index.js",
	"editorStyle": "file:./index.css",
	"style": "file:./style-index.css",
	"viewScript": "file:./view.js"
}

We’re allowing four block types: core/buttons, core/heading, core/paragraph, and core/list. These cover the content types that make sense inside a notice. A heading for the title, paragraphs for the message, buttons for a user to take action, and a list for cases where someone needs to present a few bullet points (think error validation messages or a checklist of steps).

After saving, rebuild and refresh the editor. Insert a Notice block and try adding a new inner block. You’ll see that the block inserter now only shows these four options along with 2 newer variations for the paragraph and heading blocks:

Block inserter showing only Buttons, Heading, Paragraph, and List as available blocks inside the Notice, along with block variations for stretchy paragraphs and headings.

If you try typing /image or /embed in the inner blocks area, nothing will come up. The restriction works in both the inserter panel and the slash command interface.

Adding styles for lists and buttons

You might be wondering why we included core/list and core/button when we didn’t write any specific CSS for it in Part 7. Lists are a natural fit for notice content. Think about an error notice that says “Please fix the following issues:” followed by a bulleted list. Or a success notice with a list of completed steps.

Since we added core/list to our allowed blocks, let’s add a small CSS rule for list blocks inside the notice. This isn’t strictly necessary (lists will inherit the notice’s text color and size), but it makes the output a bit more polished.

We can also add some basic styles to ensure the button matches the style of the notice. Styling buttons is more nuanced than these other typography elements since their styles are heavily dependent on the theme and buttons can have, well, a lot of different looks. We’ll add some basic considerations for background-color, color and include hover/focus states.

Add this to the &__children selector in style.scss:

src/blocks/notice/style.scss
.wp-block-lwpd-notice {
  // ... other styles
  
  &__children {
  	
  	.wp-block-heading {
  		font-size: 1.125em;
  		font-weight: 700;
  	}
  	
  	p {
  		font-size: inherit;
  	}
  	
  	a {
  		color: var(--notice-accent);
  	}
  
  	.wp-block-list {
  		font-size: inherit;
  		margin: 0;
  		padding-inline-start: 1.25em;
  	}
  	
	  .wp-block-button__link {
		  background-color: var(--notice-color);
		  color: var(--notice-bg);
		  
		  &:where(:hover, :focus) {
			  background-color: color-mix(in srgb, var(--notice-color), #fff 10%);
		  }
	  }  	
  }

  // ... other styles
}

For the list block, we’re setting font-size: inherit so the list matches the notice’s text size, resetting the margin, and using padding-inline-start for the list indentation. The 1.25em value gives enough space for the bullet markers without pushing the text too far from the left edge.

For the buttons, we’re using our CSS variables to set the background-color and color and lighten the background-color by about 10% when the button is hovered or focused. The styles have sufficient color contrast to pass WCAG AA standards, which means they’re nice and easy to read even for someone with impaired vision1.

Once you save the styles and they finish building, add a button and list to a notice block and see how it looks.

Understanding template locking

In addition to restricting inner blocks, you could also add some kind of block locking. This kind of thing isn’t always necessary, but in some cases you may find it useful when developing custom blocks.

Here’s a few use cases where locking might make sense:

Testimonial block: A testimonial usually has a fixed structure: a quote, an author name, and maybe a photo. Using templateLock: "all" here ensures that structure stays consistent. Users fill in the content but can’t add random blocks or remove the author attribution.

Pricing card: A pricing card with a title, price, feature list, and CTA button benefits from templateLock: "all" or templateLock: "insert" to keep the layout predictable. Allowing users to rearrange elements ("insert") might make sense if they want the button above the feature list, for example.

Hero section: A hero with a heading, subheading, and button group might use templateLock: "insert" so the structure stays intact but users can reorder elements if needed.

The key question is always: does the block’s purpose require a specific structure, or is it more of a general container? Notices lean toward container behavior (albeit a constrained one), so flexibility makes sense here.

Template lock levels

Template locks can be applied at different “levels” which indicates what the behavior of the lock should be. Here are the current available values as of WordPress 6.9:

all: Prevents inserting new blocks, moving existing blocks, and removing blocks. The inner block structure is completely locked down. Users can still edit the content of the blocks (typing in the heading or paragraph), but they can’t change the structure.

insert: Prevents inserting new blocks and removing existing ones, but allows users to rearrange (move) the blocks that are already there.

contentOnly: The strictest option. Prevents all structural operations and hides blocks that don’t contain editable content from the List View. Users can only edit text and media. Unlike the other lock types, child blocks cannot override this.

false: Explicitly unlocks the inner blocks area. This is useful when a parent block has locking applied and you want a specific child to opt out of it.

If you don’t set templateLock at all, the inner blocks area inherits the locking behavior of its parent. If there is no parent, it uses the locking configuration of the current post type.

How to lock specific blocks

Let’s go over a hypothetical use case for block or template locking.

You give it some thought and you conclude the block should always have a heading of some kind to create a nice visual hierarchy. It would be nice to make sure it doesn’t get accidentally removed while still providing flexibility for users.

We can achieve this kind of middle ground user-experience by defining the lock attribute on the heading blocks in our template. By only applying it to the core/heading block specifically, we can still allow users to add other allowed blocks as they see fit.

src/blocks/notice/variations.js
import { __ } from '@wordpress/i18n';
import { info, check, caution, cancelCircleFilled } from '@wordpress/icons';

const variations = [
	{
		name: 'notice-info',
		icon: info,
		title: __( 'Info Notice', 'lwpd' ),
		description: __( 'An informational notice.', 'lwpd' ),
		attributes: {
			className: 'is-style-info',
		},
		innerBlocks: [
			[
				'core/heading',
				{
					level: 2,
					content: __( 'Info', 'lwpd' ),
					lock: { move: true, remove: true },
				},
			],
			[
				'core/paragraph',
				{
					content: __( 'Add your info message here.', 'lwpd' ),
				},
			],
		],
		scope: [ 'inserter', 'transform' ],
		isDefault: true,
		isActive: [ 'className' ],
	},
	{
		name: 'notice-success',
		icon: check,
		title: __( 'Success Notice', 'lwpd' ),
		description: __( 'A success confirmation notice.', 'lwpd' ),
		attributes: {
			className: 'is-style-success',
		},
		innerBlocks: [
			[
				'core/heading',
				{
					level: 2,
					content: __( 'Success', 'lwpd' ),
					lock: { move: true, remove: true },					
				},
			],
			[
				'core/paragraph',
				{
					content: __( 'Add your success message here.', 'lwpd' ),
				},
			],
		],
		scope: [ 'inserter', 'transform' ],
		isActive: [ 'className' ],
	},
	{
		name: 'notice-warning',
		icon: caution,
		title: __( 'Warning Notice', 'lwpd' ),
		description: __( 'A warning notice for important caveats.', 'lwpd' ),
		attributes: {
			className: 'is-style-warning',
		},
		innerBlocks: [
			[
				'core/heading',
				{
					level: 2,
					content: __( 'Warning', 'lwpd' ),
					lock: { move: true, remove: true },					
				},
			],
			[
				'core/paragraph',
				{
					content: __( 'Add your warning message here.', 'lwpd' ),
				},
			],
		],
		scope: [ 'inserter', 'transform' ],
		isActive: [ 'className' ],
	},
	{
		name: 'notice-error',
		icon: cancelCircleFilled,
		title: __( 'Error Notice', 'lwpd' ),
		description: __( 'An error or alert notice.', 'lwpd' ),
		attributes: {
			className: 'is-style-error',
		},
		innerBlocks: [
			[
				'core/heading',
				{
					level: 2,
					content: __( 'Error', 'lwpd' ),
					lock: { move: true, remove: true },
				},
			],
			[
				'core/paragraph',
				{
					content: __( 'Add your error message here.', 'lwpd' ),
				},
			],
		],
		scope: [ 'inserter', 'transform' ],
		isActive: [ 'className' ],
	},
];

export default variations;

How to lock all inner blocks

While we’re not trying to do this with our Notice block, let’s talk about how you would go about locking the inner blocks if you needed to.

To lock all the inner blocks, you’ll need to set a few props on useInnerBlocksProps in the block’s edit.js file.

Template locking is set as a prop on useInnerBlocksProps. In our edit.js, it would look like this:

js

edit.js
const innerBlocksProps = useInnerBlocksProps(
	{},
	{
		template: TEMPLATE, // The default blocks to be inserted.
		templateLock: 'all', // The template locking level.
	}
);

That’s it. With templateLock: "all" set, users won’t be able to add, remove, or move any of the inner blocks. They can still edit the content inside each block though.

If you wanted to lock the template but allow individual blocks to opt out, you can use the block-level locking approach above in combination with this template lock.

Block-level locks take priority over the template lock, unless the template lock is set to contentOnly. This gives you fine-grained control when you need the overall structure locked but want certain blocks to remain flexible.

Who Can Lock and Unlock Blocks?

By default, any administrator can lock and unlock blocks through the block editor UI. If you select a block and open its options menu (the three dots in the block toolbar), you’ll see a “Lock” or “Unlock” option. This lets users toggle move and remove locking on individual blocks.

By default, if a user can use the block editor, they can unlock blocks. If you need to restrict who can use the locking UI, WordPress provides the block_editor_settings_all filter. This is a more advanced feature that may be useful when you’re building sites for clients. That said, it’s not the kind of thing you’d bundle in with a block or other plugin you’re building for wide distribution.

PHP
add_filter( 'block_editor_settings_all', function( $settings ) {
	// Only allow administrators to lock/unlock blocks
	$settings['canLockBlocks'] = current_user_can( 'activate_plugins' );
	return $settings;
} );

This is especially useful on client sites where you want editors to work within the constraints you’ve set without being able to unlock things themselves.

In practice though, most users are administrators, so doing this cleanly requires a bit of extra work. Here’s an example of how this might look at a high-level:

  • Add an options/settings page in the dashboard
  • Add a multiselect dropdown that includes all users on the site. Selected users will be able to unlock blocks.
  • Add logic in your filter callback to check the option and see if the user is in the list.

Here’s a quick example of how that might look:

PHP
add_filter( 'block_editor_settings_all', function( $settings ) {
  $full_privilege_users = get_option( 'full_privilege_users', [] );
  
  // If no users set, make no changes to the settings.
  if( !$full_privilege_users ) {
    return $settings;
  }
  
  $user_id = get_current_user_id();
  
  // Set canLockBlocks to true if the user_id is in the option array.
	$settings['canLockBlocks'] = in_array( $user_id, $full_privilege_users, true );
	
	return $settings;
} );

Quick Recap

Here’s what we covered in this part:

  • Added allowedBlocks to block.json to restrict inner blocks to headings, paragraphs, buttons, and lists.
  • Discussed the two ways to set allowed blocks: block.json (static) and the useInnerBlocksProps prop (dynamic).
  • Explored template locking options (all, insert, contentOnly, false) and when each one makes sense.
  • Decided not to use template locking for our Notice block since allowedBlocks provides enough constraint while keeping the block flexible.
  • Discussed how to use block-level locking and a practical example for when you’d want to do so.
  • Covered how to restrict who can lock and unlock blocks using the block_editor_settings_all filter.
  • Added a small CSS rule for list blocks inside the notice.

The combination of allowedBlocks and template locking gives you a lot of control over the editing experience. For our Notice block, we’re using the lighter touch: restrict which blocks are allowed, ensure the heading can only be removed intentionally, but let users decide how to arrange the inner blocks. For blocks with stricter structural requirements, template locking is the tool to reach for.

Next Steps

In Part 9 , we’ll look at how to add custom toolbar controls to the Notice block, giving users quick access to common actions right from the block toolbar.

Further Reading

  1. This kind of accessibility consideration is crucial for designing anything a user consumes on the page. Before you finish writing styles, use the browser dev tools to check if the text has sufficient color contrast or not. ↩︎


Leave a Reply

Your email address will not be published. Required fields are marked *