Contents
Introduction
This article is a complete, practical tutorial for creating a Gutenberg parent block that uses InnerBlocks and includes custom controls built in JavaScript. It covers every step: project setup, block metadata, editor and save code, InspectorControls, attributes, styling (editor and front-end), advanced patterns (programmatically changing inner blocks), PHP registration for plugin/theme, build scripts, and troubleshooting tips. Code examples are provided for all key files and must be dropped into your project exactly as shown.
What you will build
- A parent block that exposes an InnerBlocks area for content editors.
- Custom InspectorControls (controls in the block sidebar) that toggle settings such as number of grid columns, background color, padding, and a layout preset.
- Live editor preview reflecting the controls, and persistent attributes saved into post content so rendered front-end output matches the editor.
- Optional advanced logic to transform inner blocks when the column count changes.
Prerequisites
- WordPress 5.8 recommended (block.json-based registration works well).
- Node.js and npm (or yarn).
- Familiarity with JavaScript (ESNext), React, and basic WordPress development.
- @wordpress/scripts for building the block (recommended), or your own Webpack/Babel setup.
Folder structure (recommended)
my-block/ ├─ block.json ├─ src/ │ ├─ index.js │ ├─ editor.js │ ├─ save.js │ ├─ style.scss │ └─ editor.scss ├─ build/ (auto-generated) ├─ package.json └─ my-block.php (or register.php if part of plugin)
block.json (metadata)
{ apiVersion: 2, name: my-plugin/grid-innerblocks, title: Grid with InnerBlocks and Controls, category: design, icon: grid-view, description: A parent block that wraps InnerBlocks and exposes custom Inspector controls., supports: { html: false }, attributes: { columns: { type: number, default: 3 }, gap: { type: number, default: 16 }, backgroundColor: { type: string, default: }, layout: { type: string, default: default }, isFullWidth: { type: boolean, default: false } }, editorScript: file:./build/index.js, editorStyle: file:./build/editor.css, style: file:./build/style.css }
Key concept: attributes vs InnerBlocks
Attributes store block-level settings (columns, gap, background, toggle values). InnerBlocks stores nested block content. Use attributes to control layout and appearance while leaving content editing to InnerBlocks. In the editor, you apply CSS or classNames based on attributes in save, you render markup that uses the same attributes so front-end output matches.
Editor component (src/editor.js)
This file contains the edit() component. It uses useBlockProps, InnerBlocks, InspectorControls, PanelBody and various control components. It also demonstrates a CSS grid layout in the editor using the block attribute columns.
/ src/editor.js / import { useEffect } from @wordpress/element import { __ } from @wordpress/i18n import { InspectorControls, InnerBlocks, useBlockProps } from @wordpress/block-editor import { PanelBody, RangeControl, ToggleControl, SelectControl, ColorPalette } from @wordpress/components const TEMPLATE = [ [ core/paragraph, { placeholder: Column 1 } ], [ core/paragraph, { placeholder: Column 2 } ], [ core/paragraph, { placeholder: Column 3 } ], ] export default function Edit({ attributes, setAttributes }) { const { columns, gap, backgroundColor, layout, isFullWidth } = attributes // Block props for wrapper const blockProps = useBlockProps({ className: my-grid-block layout-{layout} {isFullWidth ? is-fullwidth : }, style: { backgroundColor: backgroundColor undefined, gap: gap ? {gap}px : undefined, gridTemplateColumns: repeat({columns}, 1fr), }, }) return ( <>setAttributes({ columns: value })} min={1} max={6} /> setAttributes({ gap: value })} min={0} max={80} /> setAttributes({ layout: value })} /> setAttributes({ isFullWidth: val })} /> {__(Background, my-plugin)}setAttributes({ backgroundColor: color })} /> > ) }
Save component (src/save.js)
The save function writes the front-end HTML. For static blocks (not dynamic/PHP-rendered), use InnerBlocks.Content to output nested blocks saved content. Ensure the wrapper attributes that affect display (columns, gap, backgroundColor, etc.) are applied to the wrapper so the front-end matches the editor.
/ src/save.js / import { InnerBlocks, useBlockProps } from @wordpress/block-editor export default function save({ attributes }) { const { columns, gap, backgroundColor, layout, isFullWidth } = attributes const blockProps = useBlockProps.save({ className: my-grid-block layout-{layout} {isFullWidth ? is-fullwidth : }, style: { backgroundColor: backgroundColor undefined, gap: gap ? {gap}px : undefined, gridTemplateColumns: repeat({columns}, 1fr), }, }) return () }
Entry point (src/index.js)
/ src/index.js / import { registerBlockType } from @wordpress/blocks import edit from ./editor import save from ./save import ./style.scss import ./editor.scss import metadata from ./block.json registerBlockType(metadata.name, { ...metadata, edit, save })
Front-end styles (src/style.scss)
/ style.scss - front-end / .my-grid-block { display: grid / gap overridden inline by attribute / align-items: start margin-bottom: 1rem } .my-grid-block .wp-block { / Individual inner block reset / background: transparent padding: 0 } / Layout preset examples / .my-grid-block.layout-card .wp-block { background: #fff border: 1px solid rgba(0,0,0,0.06) padding: 16px border-radius: 6px } .my-grid-block.is-fullwidth { width: 100% margin-left: 0 margin-right: 0 }
Editor-only styles (src/editor.scss)
/ editor.scss - styles applied in the editor / .my-grid-block { min-height: 80px transition: background-color 120ms ease, gap 120ms ease border: 1px dashed rgba(0,0,0,0.08) padding: 8px }
Optional: programmatically adjust inner blocks when columns change (advanced)
Sometimes you want to actively change the number of inner columns by creating/removing child blocks when the columns control changes. The following example demonstrates using useEffect plus replaceInnerBlocks from the block editor store. Use this only if you want to manage actual nested block instances — otherwise prefer CSS grid with repeat(columns, 1fr).
/ Advanced: adjust inner blocks based on columns count / import { useEffect } from @wordpress/element import { useSelect, useDispatch, select } from @wordpress/data import { createBlock } from @wordpress/blocks import { InnerBlocks, useBlockProps } from @wordpress/block-editor function Edit({ clientId, attributes, setAttributes }) { const { columns } = attributes const { replaceInnerBlocks } = useDispatch(core/block-editor) // Read current inner blocks const innerBlocks = useSelect( (select) => select(core/block-editor).getBlocks(clientId), [clientId] ) // When columns change, create new inner blocks if count differs useEffect(() => { if (!innerBlocks) return const currentCount = innerBlocks.length if (currentCount === columns) return const blocks = [] for (let i = 0 i < columns i ) { blocks.push(createBlock(core/paragraph, { placeholder: Column {i 1} })) } // replaceInnerBlocks will replace all inner blocks of this block replaceInnerBlocks(clientId, blocks, false) }, [columns, innerBlocks, clientId, replaceInnerBlocks]) return () }
Register block in PHP (plugin or theme)
If you build a plugin or include the block in a theme, register the block via PHP. With block.json and built assets present, calling register_block_type with the directory will auto-register scripts/styles.
package.json (build scripts, using @wordpress/scripts)
{ name: my-grid-innerblocks, version: 1.0.0, scripts: { start: wp-scripts start, build: wp-scripts build, lint: wp-scripts lint-js }, devDependencies: { @wordpress/scripts: ^28.0.0 } }
Development workflow
- Run npm install to install @wordpress/scripts and other dependencies.
- Run npm run start during development for a dev server with hot reloading (or watch).
- Run npm run build to produce production-ready assets in the build/ folder (block.json references these files with file: paths).
- Activate the plugin (or place files into your theme and let register_block_type include them).
- Insert the block in the Block Editor and test controls, InnerBlocks content, and front-end rendering.
Allowed child blocks and templates
- allowedBlocks limits what editors can insert into InnerBlocks. Provide a whitelist to keep content predictable.
- template can create default child blocks when a parent is inserted. Templates are arrays of [name, attributes, innerTemplate].
- templateLock controls whether inner blocks can be moved/removed/inserted. Use false, insert, or all.
Common attributes and types
- string: text or color values
- number: numeric settings like columns, gap
- boolean: toggles
- object/array: more complex settings (store small objects only avoid large HTML)
Best practices
- Keep attributes minimal. Put content in InnerBlocks — not in large string attributes — so nested blocks remain editable and portable.
- Use inline styles for dynamic properties (grid-template-columns, gap) if you prefer per-instance values, or map attributes to CSS custom properties for less inline markup.
- Prefer front-end CSS (style.scss) to mirror editor styles so the preview is faithful.
- Sanitize attributes if you accept user input from non-trusted sources in dynamic blocks.
- Use i18n functions like __() for strings in the editor source.
Accessibility considerations
- Ensure inner blocks have sensible focus order and keyboard navigation the grid layout should not interfere with natural keyboard flow.
- Provide descriptive labels for controls in InspectorControls and use aria- labels if exposing complex interactive controls.
- If using custom controls (e.g., color pickers), ensure sufficient contrast for content inside the grid and offer accessible defaults.
Internationalization (i18n)
Wrap user-visible strings in translation functions from @wordpress/i18n (e.g., __, _x) and build translation-ready plugin headers if distributing the block.
Troubleshooting
- If the block does not appear in the editor, check browser console for script errors and ensure build assets are loaded (verify block.json paths or register_block_type call).
- If InnerBlocks.Content doesnt output nested content on the front-end, check that nested blocks are saved (inspect post content) and that save() returns InnerBlocks.Content.
- If Inspector controls do not update the editor preview, ensure setAttributes is called and attributes are declared in block.json (apiVersion 2) or in registerBlockType attributes.
- Use useSelect / useDispatch from @wordpress/data if you need to query or modify blocks programmatically (advanced).
Advanced tips and patterns
- Use CSS custom properties in style.scss and set them inline in save() so theme styles can override defaults while per-instance values still work:
/ Example using custom property / .my-grid-block { --my-grid-gap: 16px gap: var(--my-grid-gap) }
// In save(): style: { --my-grid-gap: gap ? {gap}px : undefined }
- If you want responsive column counts, store an object attribute like columns: { desktop: 3, tablet: 2, mobile: 1 } and apply responsive CSS classes or inline style via media queries compiled into your theme or style.css.
- For dynamic rendering that depends on server-side data, register the block server-side as a render_callback via register_block_type and output server-generated markup but keep InnerBlocks for editable nested content only when appropriate.
Complete minimal example recap
– block.json defines attributes and assets.
– src/editor.js implements controls and InnerBlocks usage with useBlockProps for wrapper styling.
– src/save.js writes matching front-end markup with InnerBlocks.Content and inline styles for dynamic properties.
– style.scss and editor.scss keep editor and front-end visual parity.
– register_block_type( __DIR__ ) in PHP registers the block when build assets are present.
Useful references
Final notes
This tutorial intentionally favors a CSS-driven approach to control layout (columns via grid-template-columns) for predictability and simplicity. Use programmatic inner block manipulation only when content instances must be added or removed automatically. The examples are production-ready patterns: you can copy the code into a plugin scaffold, run npm run build, and register the block in WordPress.
|
Acepto donaciones de BAT's mediante el navegador Brave 🙂 |