Building a custom Gutenberg block with InnerBlocks and supports
Gutenberg blocks become significantly more useful when they can contain other blocks. A custom layout block can provide a consistent wrapper, spacing rules and responsive behaviour while allowing editors to add headings, paragraphs, buttons, images or reusable components inside it. The InnerBlocks component is the standard way to create this nested editing experience.
The Supports API complements InnerBlocks by exposing WordPress features such as custom colours, padding, margin, typography, alignment and HTML anchors. Instead of building separate controls for every design option, a block can declare the capabilities it supports in block.json, allowing the editor to use familiar sidebar controls.
This guide builds a practical “content panel” block from the ground up. The example suits a marketing page, WooCommerce promotion, community notice or local service page, whether the site serves customers in Sydney, a trades business in Perth or a not-for-profit in regional New South Wales.
Plan the block structure
A useful nested block should have a clear responsibility. In this example, the outer block creates a panel with a background colour, border radius and spacing. Its inner content remains editable, so an administrator can insert a heading, text, image, list or call-to-action without changing the block’s code.
The block will use a locked template containing a heading and paragraph. The template gives editors a helpful starting point while still allowing additional blocks. A templateLock value of false keeps the layout flexible; use "all" when the design must never change, or "insert" when existing blocks should remain fixed but new blocks may be added.
Create a directory such as custom-content-panel inside a plugin or block source project. The basic files are:
custom-content-panel/
├── block.json
├── edit.js
├── index.js
├── save.js
└── style.css
A plugin is usually preferable to placing this functionality in a theme. The block will remain available if the site changes themes, which matters for Australian businesses that refresh their branding or move from a brochure site to WooCommerce.
Configure block.json and supports
The block.json file defines the block name, scripts, editor assets and supported features. WordPress reads this metadata during registration, and modern versions can automatically load the editor and frontend styles declared in the file.
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "acme/content-panel",
"version": "1.0.0",
"title": "Content panel",
"category": "design",
"icon": "layout",
"description": "A flexible panel containing nested blocks.",
"supports": {
"html": false,
"anchor": true,
"align": ["wide", "full"],
"color": {
"background": true,
"text": true,
"gradients": true,
"link": true
},
"spacing": {
"margin": true,
"padding": true,
"blockGap": true
},
"typography": {
"fontSize": true,
"lineHeight": true
}
},
"editorScript": "file:./index.js",
"style": "file:./style.css"
}
The html setting prevents the editor from exposing a raw HTML editing mode for the whole block. The anchor setting adds an HTML ID field, which is useful for jump links on long pages. Alignment support lets the panel use wide or full-width layouts, while spacing support provides responsive-friendly design controls without custom React components.
Supports values should be chosen carefully. A full set of colour and typography controls may overwhelm a simple block, and unrestricted options can produce inconsistent branding. A council information site in Brisbane may need controlled colours for accessibility, while a creative studio in Melbourne may want gradients and custom font sizes.
The block must also be registered from PHP. With a build process that outputs build, registration can be as simple as:
<?php
add_action( 'init', function () {
register_block_type( __DIR__ . '/build' );
} );
If the block lives in a plugin, place this code in the main plugin file or an included PHP file. The build/block.json path must match the location produced by @wordpress/scripts.
Add InnerBlocks to the editor
The edit.js file controls the editor interface. useBlockProps() applies classes, styles and support attributes generated by WordPress. InnerBlocks then creates the editable area inside the outer wrapper.
import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';
const TEMPLATE = [
[
'core/heading',
{
level: 2,
placeholder: 'Add a panel heading…'
}
],
[
'core/paragraph',
{
placeholder: 'Explain the offer, service or announcement…'
}
]
];
export default function Edit() {
const blockProps = useBlockProps();
return (
<div { ...blockProps }>
<InnerBlocks
template={ TEMPLATE }
templateLock={ false }
orientation="vertical"
/>
</div>
);
}
The template is inserted when the block is first added. orientation="vertical" tells the editor that child blocks are arranged in a column, which helps drag-and-drop behaviour. The child blocks inherit the editor’s normal controls, so there is no need to recreate paragraph alignment, heading levels or image selection.
To restrict the available children, add an allowedBlocks array:
<InnerBlocks
allowedBlocks={ [
'core/heading',
'core/paragraph',
'core/image',
'core/buttons',
'core/list'
] }
template={ TEMPLATE }
/>
This is useful when the panel has a defined purpose. A pricing card may allow buttons and lists but reject another group block. A local café in Adelaide could use a controlled menu panel with headings, descriptions and images, keeping the editor experience straightforward for staff who update the site between busy service periods.
An alternative is to expose a block variation or a custom inserter button. For most content panels, the standard inserter is enough. The key principle is to let InnerBlocks manage nested block attributes and editor behaviour rather than storing child content in a custom string attribute.
Render and style the nested content
The save.js file serialises the wrapper and its children. InnerBlocks.Content outputs the saved markup for every nested block, while useBlockProps.save() adds the classes, styles and attributes generated by the Supports API.
import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';
export default function save() {
return (
<div { ...useBlockProps.save() }>
<InnerBlocks.Content />
</div>
);
}
The matching index.js registers the editor and save functions:
import { registerBlockType } from '@wordpress/blocks';
import metadata from './block.json';
import Edit from './edit';
import save from './save';
registerBlockType( metadata.name, {
...metadata,
edit: Edit,
save
} );
A simple stylesheet can provide a usable baseline without fighting user-selected support values:
.wp-block-acme-content-panel {
box-sizing: border-box;
max-width: 100%;
}
.wp-block-acme-content-panel > *:first-child {
margin-top: 0;
}
.wp-block-acme-content-panel > *:last-child {
margin-bottom: 0;
}
.wp-block-acme-content-panel :where(a) {
text-underline-offset: 0.15em;
}
@media (max-width: 600px) {
.wp-block-acme-content-panel {
padding: 1.25rem;
}
}
Avoid setting a fixed background, text colour or padding in CSS when the Supports API controls those properties. WordPress writes the selected values as classes or inline styles. Hard-coded declarations with high specificity can make the sidebar controls appear broken.
For a dynamic block, replace save.js with render.php and set "render": "file:./render.php" in block.json. Dynamic rendering is appropriate when the panel includes changing query results, membership data or server-side WooCommerce information. A static block is simpler for editorial content and creates less work during page rendering.
Choose supports for a reliable editor experience
The following settings cover common requirements for an editorial panel. They should be treated as design decisions rather than a checklist to enable blindly.
| Feature | Metadata setting | Practical benefit | Common caution |
|---|---|---|---|
| Custom colours | "color": { "background": true, "text": true } |
Lets editors adjust panel contrast and branding | Check colour contrast and theme presets |
| Spacing controls | "spacing": { "padding": true, "margin": true } |
Supports responsive separation without custom controls | Large values can damage mobile layouts |
| Alignment | "align": ["wide", "full"] |
Allows broad panels across a page | Full-width layouts need sensible inner widths |
| Typography | "typography": { "fontSize": true } |
Provides reusable text size presets | Too many sizes weaken visual hierarchy |
| HTML anchors | "anchor": true |
Enables links to a specific panel | IDs must be unique on the page |
| Custom class names | "customClassName": true |
Helps developers add targeted styling | Class names should be documented and maintained |
| Nested blocks | InnerBlocks in edit.js and save.js |
Gives editors flexible content inside the wrapper | Avoid unrestricted nesting where layouts can break |
Spacing support is particularly useful for Australian sites that need to behave well across devices and connections. Visitors may load a page over mobile data in outer Sydney, on a slower regional NBN service or while travelling between towns. A responsive block with modest defaults is safer than one that depends on large fixed dimensions.
Support options also affect saved markup. When a user chooses a background colour, WordPress may add a class such as has-blue-background-color; custom CSS should respect those generated classes. If a block must enforce a particular colour for legal or brand reasons, omit that support and provide a controlled attribute or style variation instead.
Test accessibility, output and maintenance
Test the block in both the editor and the frontend. Add it to a page, insert several child blocks, change alignment and colours, set padding, reload the editor and confirm that all values persist. Check the generated HTML in the browser and verify that the wrapper has the expected wp-block-acme-content-panel class.
Accessibility testing should include keyboard navigation, visible focus states, heading order, contrast and mobile zoom. Supports API controls make it easy for an editor to choose a pale background with light text, so the site should provide sensible theme presets and document acceptable combinations. WCAG checks are especially important for public-facing services, health providers and government-related organisations across Australia.
The nested content itself should remain semantic. Use a heading for a section title, paragraphs for prose, lists for grouped items and buttons for actions. Do not place every piece of text in a heading simply because the heading has a convenient style. If the panel is used for a promotion, a button leading to a booking page is clearer than a linked paragraph.
Links inside nested blocks work normally. For example, a panel may contain an educational resource link such as a rules guide when the surrounding content is documenting game-related terminology or building a reference card. The link should be editorially relevant, use descriptive anchor text and open in a new tab only when there is a clear reason.
When a block fails validation, compare the markup generated by save.js with the markup stored in the post. Common causes include changing wrapper elements, altering class names, rendering different default attributes or adding unsupported dynamic values. If the issue is difficult to isolate, WordPress troubleshooting references such as WordPress Q&A can help identify whether the problem comes from the block, a theme or another plugin.
Keep the block’s public markup stable once it is used on production pages. If a future version needs a different structure, add a deprecation definition rather than silently changing old content. Build the assets with npm run build, test against the WordPress versions supported by the site, and review the final panel on common Australian viewport sizes, including narrow mobile screens and wide desktop displays.