Anatomy of a Block
01 — Manifest
block.json
Declares the block’s name, title, category, icon, attributes, and supports. The single source of truth for both JS and PHP.
block.json
02 — Editor
edit.js
The React component rendered inside the Gutenberg editor. Uses block props, attributes, and inspector controls to build the editing UI.
edit.js
03 — Output
save.js / render.php
For static blocks, save.js serializes the HTML. For dynamic blocks, render.php generates output on the server at request time.
save.js
render.php
04 — Registration
index.js + PHP
index.js calls registerBlockType() on the client. PHP calls register_block_type() pointing at block.json on the server.
index.js
plugin.php
05 — Styles
style.scss / editor.scss
style.scss loads on both front-end and editor. editor.scss loads only inside Gutenberg — use it for editor-only chrome and layout overrides.
style.scss
editor.scss
06 — Tooling
@wordpress/scripts
Zero-config build toolchain. Bundles JS/SCSS via webpack, handles HMR for the editor, and outputs the compiled assets WordPress expects.
package.json
block.json — The Block Manifest
manifest
Full block.json Example
A complete block.json file covering name, metadata, attributes, supports, and script/style handles. WordPress reads this to register everything automatically.
{
“$schema”: “https://schemas.wp.org/trunk/block.json”,
“apiVersion”: 3,
“name”: “myplugin/hero”,
“version”: “1.0.0”,
“title”: “Hero Banner”,
“category”: “media”,
“icon”: “cover-image”,
“description”: “A full-width hero with heading and CTA.”,
“keywords”: [“hero”, “banner”, “header”],
“textdomain”: “myplugin”,
“attributes”: {
“heading”: { “type”: “string”, “default”: “Hello” },
“align”: { “type”: “string”, “default”: “wide” },
“showButton”: { “type”: “boolean”, “default”: true },
“buttonLabel”: { “type”: “string”, “default”: “Learn more” }
},
“supports”: {
“html”: false,
“align”: [“wide”, “full”],
“color”: { “background”: true, “text”: true },
“spacing”: { “padding”: true, “margin”: true },
“typography”: { “fontSize”: true }
},
“editorScript”: “file:./index.js”,
“editorStyle”: “file:./editor.css”,
“style”: “file:./style.css”,
“render”: “file:./render.php”
}
manifest
Registering via PHP
Point
register_block_type() at your block’s directory. WordPress reads block.json automatically — no need to manually enqueue scripts or styles.// plugin.php or functions.php
add_action( ‘init’, function() {
// Register a single block
register_block_type(
__DIR__ . ‘/build/hero’
);
// Register all blocks in a directory
$blocks = glob(
__DIR__ . ‘/build/*/block.json’
);
foreach ( $blocks as $block ) {
register_block_type(
dirname( $block )
);
}
});
JS
Registering on the Client
index.js imports edit and save, then calls registerBlockType() with the block name matching block.json exactly. Metadata is imported from block.json directly.
// index.js
import { registerBlockType } from ‘@wordpress/blocks’;
import metadata from ‘./block.json’;
import Edit from ‘./edit’;
import save from ‘./save’;
registerBlockType( metadata.name, {
edit: Edit,
save,
});
Attributes — Types & Sources
| Type | Source | Description & Example |
|---|---|---|
| string | — | Plain text value. Stored in block comment delimiter. Use for headings, labels, URLs. |
| number | — | Integer or float. Use for counts, sizes, or numeric settings like columns or opacity. |
| boolean | — | True/false toggle. Use for show/hide controls, feature flags, or alignment toggles. |
| array | — | Array of values. Use for multi-select options, repeating items, or tag lists. |
| object | — | Structured data. Use for image objects (id, url, alt), link objects, or complex settings. |
| string | html | Reads value from the saved block HTML using a CSS selector. "selector": "p" |
| string | attribute | Reads an HTML attribute from the saved markup. e.g. src from an img tag. |
| string | text | Reads the text content of an element. Use with RichText — strips all HTML tags. |
| array | query | Reads repeated elements from saved HTML into an array of objects via a query selector. |
| string | meta | Reads from post meta. Requires the meta key to be registered and REST-exposed. |
Edit & Save Components
JS — edit
The Edit Component
Receives
attributes and setAttributes as props. Renders the interactive editor UI. Can use all React hooks and WP components freely.// edit.js
import { useBlockProps, RichText }
from ‘@wordpress/block-editor’;
export default function Edit({ attributes, setAttributes }) {
const blockProps = useBlockProps({
className: ‘my-hero’,
});
return (
<div { …blockProps }>
<RichText
tagName=”h2″
value={ attributes.heading }
onChange={ val =>
setAttributes({ heading: val })
}
placeholder=”Enter heading…”
/>
</div>
);
}
JS — save
The Save Function
A pure function that serializes block state to static HTML. Has no access to server data. Must be deterministic — the same attributes must always produce the same markup.
// save.js
import { useBlockProps, RichText }
from ‘@wordpress/block-editor’;
export default function save({ attributes }) {
const blockProps =
useBlockProps.save({ className: ‘my-hero’ });
return (
<div { …blockProps }>
<RichText.Content
tagName=”h2″
value={ attributes.heading }
/>
{ attributes.showButton && (
<a href=”#” className=”hero__btn”>
{ attributes.buttonLabel }
</a>
)}
</div>
);
}
PHP — dynamic
Dynamic Block — render.php
For dynamic blocks, set
"render": "file:./render.php" in block.json and return null from save(). WordPress calls render.php with $attributes, $content, and $block.<?php
// render.php — $attributes, $content, $block available
$heading = esc_html(
$attributes[‘heading’] ?? ‘Hello’
);
$show_btn = ! empty( $attributes[‘showButton’] );
$wrapper = get_block_wrapper_attributes([
‘class’ => ‘my-hero’,
]);
?>
<div <?php echo $wrapper; ?>>
<h2><?php echo $heading; ?></h2>
<?php if ( $show_btn ) : ?>
<a href=”#” class=”hero__btn”>
<?php echo esc_html(
$attributes[‘buttonLabel’] ?? ‘Read more’
); ?>
</a>
<?php endif; ?>
</div>
PHP — deprecated
Dynamic Block — register callback
The older pattern before render.php: pass a
render_callback directly to register_block_type(). Still valid — useful for keeping render logic in PHP class methods.register_block_type(
__DIR__ . ‘/build/hero’,
[
‘render_callback’ =>
function( $attrs, $content ) {
ob_start();
$heading = esc_html(
$attrs[‘heading’] ?? ‘Hello’
);
echo “<div class=’my-hero’>”;
echo ” <h2>{$heading}</h2>”;
echo “</div>”;
return ob_get_clean();
},
]
);
Inspector Controls & Block Toolbar
JS
InspectorControls
Renders a panel in the right-hand sidebar. Use
PanelBody to group related settings. Accepts any WP components like TextControl, ToggleControl, or SelectControl.import {
InspectorControls,
useBlockProps,
} from ‘@wordpress/block-editor’;
import {
PanelBody, ToggleControl, RangeControl,
} from ‘@wordpress/components’;
export default function Edit({ attributes, setAttributes }) {
return (
<>
<InspectorControls>
<PanelBody title=”Hero Settings” initialOpen>
<ToggleControl
label=”Show Button”
checked={ attributes.showButton }
onChange={ val =>
setAttributes({ showButton: val })
}
/>
<RangeControl
label=”Min Height (px)”
value={ attributes.minHeight }
onChange={ val =>
setAttributes({ minHeight: val })
}
min={ 200 } max={ 800 }
/>
</PanelBody>
</InspectorControls>
<div { …useBlockProps() }> … </div>
</>
);
}
JS
BlockControls & Toolbar
Adds controls to the floating block toolbar above the selected block. Use
ToolbarGroup and ToolbarButton for custom actions like toggling layout modes.import {
BlockControls, useBlockProps,
} from ‘@wordpress/block-editor’;
import {
ToolbarGroup, ToolbarButton,
} from ‘@wordpress/components’;
import { alignLeft, alignCenter }
from ‘@wordpress/icons’;
export default function Edit({ attributes, setAttributes }) {
return (
<>
<BlockControls>
<ToolbarGroup>
<ToolbarButton
icon={ alignLeft }
label=”Align Left”
isActive={ attributes.align === ‘left’ }
onClick={ () =>
setAttributes({ align: ‘left’ })
}
/>
<ToolbarButton
icon={ alignCenter }
label=”Align Center”
isActive={ attributes.align === ‘center’ }
onClick={ () =>
setAttributes({ align: ‘center’ })
}
/>
</ToolbarGroup>
</BlockControls>
<div { …useBlockProps() }> … </div>
</>
);
}
JS
MediaUpload — Image Picker
Opens the WP media library for image selection. Pair with
MediaUploadCheck for capability gating. Returns the selected media object to your callback.import {
MediaUpload, MediaUploadCheck,
} from ‘@wordpress/block-editor’;
import { Button } from ‘@wordpress/components’;
// Inside Edit:
<MediaUploadCheck>
<MediaUpload
onSelect={ media =>
setAttributes({
imageUrl: media.url,
imageId: media.id,
imageAlt: media.alt,
})
}
allowedTypes={ [‘image’] }
value={ attributes.imageId }
render={ ({ open }) => (
<Button variant=”secondary” onClick={ open }>
{ attributes.imageUrl
? ‘Replace Image’
: ‘Select Image’
}
</Button>
)}
/>
</MediaUploadCheck>
JS
useSelect — Reading Store Data
Pull data from any Gutenberg or WP data store inside your edit component. Use for reading current post data, editor settings, or block context.
import { useSelect } from ‘@wordpress/data’;
import { store as coreStore }
from ‘@wordpress/core-data’;
import { store as editorStore }
from ‘@wordpress/editor’;
// Inside Edit:
const { postTitle, siteTitle } = useSelect(
select => ({
postTitle: select( editorStore )
.getEditedPostAttribute(‘title’),
siteTitle: select( coreStore )
.getEntityRecord(‘root’, ‘site’)
?.title,
}),
[]
);
InnerBlocks — Nested Block Areas
inner
InnerBlocks in Edit
Drop
InnerBlocks into your edit component to create a droppable area for nested blocks. Use allowedBlocks to restrict which blocks can be inserted.import {
useBlockProps, InnerBlocks,
} from ‘@wordpress/block-editor’;
const ALLOWED = [
‘core/heading’,
‘core/paragraph’,
‘core/image’,
];
const TEMPLATE = [
[‘core/heading’, { level: 2, placeholder: ‘Title’ }],
[‘core/paragraph’, { placeholder: ‘Add content…’ }],
];
export default function Edit() {
return (
<div { …useBlockProps() }>
<InnerBlocks
allowedBlocks={ ALLOWED }
template={ TEMPLATE }
templateLock=”insert”
/>
</div>
);
}
inner
InnerBlocks.Content in Save
Serializes the nested blocks into the saved HTML output. Must be present in the save function whenever InnerBlocks is used in the edit component.
import {
useBlockProps, InnerBlocks,
} from ‘@wordpress/block-editor’;
export default function save() {
return (
<div { …useBlockProps.save() }>
<InnerBlocks.Content />
</div>
);
}
// In render.php for dynamic blocks —
// $content already contains rendered inner blocks:
// echo ‘<div ‘ . $wrapper . ‘>’ . $content . ‘</div>’;
inner
useInnerBlocksProps
The modern hook-based alternative to the InnerBlocks component. Merges block props with inner block behaviour — lets you style the inner block wrapper directly.
import {
useBlockProps,
useInnerBlocksProps,
} from ‘@wordpress/block-editor’;
export default function Edit() {
const blockProps = useBlockProps();
const innerProps = useInnerBlocksProps(
{ className: ‘my-block__inner’ },
{
allowedBlocks: [‘core/paragraph’],
template: [
[‘core/paragraph’, {}]
],
}
);
return (
<div { …blockProps }>
<div { …innerProps } />
</div>
);
}
Block Variations, Styles & Transforms
style
registerBlockStyle()
Adds a named style variant to any existing block — including core blocks. Appears as a style option in the block sidebar. Add a CSS class to match in your stylesheet.
import { registerBlockStyle }
from ‘@wordpress/blocks’;
// Add a “Pill” style to core/button
registerBlockStyle( ‘core/button’, {
name: ‘pill’,
label: ‘Pill’,
});
// Add multiple styles at once
[ ‘card’, ‘outlined’, ‘ghost’ ].forEach(
style => registerBlockStyle( ‘core/group’, {
name: style,
label: style.charAt(0).toUpperCase()
+ style.slice(1),
})
);
// Match in CSS:
// .wp-block-button.is-style-pill { border-radius: 999px; }
variation
registerBlockVariation()
Registers a pre-configured variation of a block with preset attributes and inner blocks. Appears in the block inserter as its own item — great for layout starters.
import { registerBlockVariation }
from ‘@wordpress/blocks’;
registerBlockVariation( ‘core/group’, {
name: ‘card-group’,
title: ‘Card’,
description: ‘A pre-styled card layout.’,
icon: ‘id-alt’,
attributes: {
className: ‘is-style-card’,
layout: { type: ‘constrained’ },
},
innerBlocks: [
[‘core/image’, {}],
[‘core/heading’, { level: 3 }],
[‘core/paragraph’, {}],
],
isDefault: false,
scope: [‘inserter’],
});
transform
Block Transforms
Define how a block can be converted to or from another block type. Transforms appear in the block toolbar’s “Transform to” menu. Define both
from and to arrays in your block’s transforms property.// In registerBlockType() settings:
transforms: {
from: [
{
type: ‘block’,
blocks: [‘core/paragraph’],
transform: ({ content }) => {
return createBlock(
‘myplugin/hero’,
{ heading: content }
);
},
},
],
to: [
{
type: ‘block’,
blocks: [‘core/heading’],
transform: ({ heading }) => {
return createBlock(
‘core/heading’,
{ content: heading, level: 2 }
);
},
},
],
},
filter
addFilter — Block Hooks (PHP & JS)
Extend or modify any block using WP’s hook system. PHP filters like
render_block wrap server output; JS filters like blocks.registerBlockType modify block settings in the editor.// PHP — wrap every core/paragraph output
add_filter(
‘render_block_core/paragraph’,
function( $html, $block ) {
return ‘<div class=”para-wrap”>’
. $html . ‘</div>’;
},
10, 2
);
// JS — add a default className to core/image
import { addFilter } from ‘@wordpress/hooks’;
addFilter(
‘blocks.registerBlockType’,
‘myplugin/image-defaults’,
( settings, name ) => {
if ( name !== ‘core/image’ ) return settings;
return {
…settings,
attributes: {
…settings.attributes,
className: {
…settings.attributes.className,
default: ‘is-rounded’,
},
},
};
}
);
⚠ Pro Tips
- Always run
npx @wordpress/create-blockto scaffold a new block — it wires up block.json, webpack, and all necessary files correctly from the start. - If you change a block’s
save()function without adding a deprecation, existing posts will show a block validation error. Always add adeprecatedentry before shipping save changes. - For anything that needs live data (post queries, user info, ACF fields), always build a dynamic block with
render.php— staticsave()blocks can’t fetch data at render time. - Use
get_block_wrapper_attributes()in render.php to forward editor-applied styles, spacing, and custom classes to the front-end automatically. - Keep
edit.jsandsave.jsas thin as possible — move complex logic into custom hooks or helper functions in autils/directory. - Use
@wordpress/scripts(thewp-scriptsCLI) as your build tool — it handles webpack, ESLint, Jest, and TypeScript support with zero config.
Helpful Resources & Links
Block Editor Handbook
The official end-to-end guide to Gutenberg block development — the essential starting point.
developer.wordpress.org/block-editor
Block API Reference
Full reference for block.json keys, attributes, supports, transforms, and variations.
developer.wordpress.org/block-editor/reference-guides/block-api
@wordpress/components
Full reference for every inspector and toolbar component — ToggleControl, SelectControl, ColorPicker and more.
developer.wordpress.org/block-editor/reference-guides/components
create-block scaffolder
The official zero-config CLI to scaffold a complete block plugin with block.json and wp-scripts wired up.
npmjs.com/@wordpress/create-block
GutenbergHub
Patterns, tutorials, and block examples from the community. Great for inspiration and real-world code snippets.
gutenberghub.com
Block Developer Cookbook
Recipes for common block development patterns — inner blocks, data fetching, custom controls, and more.
blockdevelopercookbook.com
Wholesome Code — Gutenberg
Deep-dive tutorials on advanced Gutenberg patterns: slots, filters, data stores, and block context.
wholesomecode.ltd
Gutenberg on GitHub
The source of truth. Browse core block implementations, packages, and raise issues directly with the team.
github.com/WordPress/gutenberg