Blocks

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 ) ); } });
register_block_type() init hook block.json
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, });
registerBlockType() @wordpress/blocks metadata import
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> ); }
useBlockProps() RichText setAttributes()
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> ); }
useBlockProps.save() RichText.Content pure function
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>
get_block_wrapper_attributes() $attributes render.php
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(); }, ] );
render_callback ob_start()
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> </> ); }
InspectorControls PanelBody ToggleControl RangeControl
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> </> ); }
BlockControls ToolbarGroup ToolbarButton
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>
MediaUpload MediaUploadCheck onSelect
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, }), [] );
useSelect() @wordpress/data core-data store
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> ); }
InnerBlocks allowedBlocks template templateLock
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>’;
InnerBlocks.Content $content (PHP)
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> ); }
useInnerBlocksProps() modern pattern
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; }
registerBlockStyle() is-style-{name}
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’], });
registerBlockVariation() innerBlocks scope
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 } ); }, }, ], },
transforms.from transforms.to createBlock()
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’, }, }, }; } );
render_block addFilter() blocks.registerBlockType

⚠ Pro Tips

  • Always run npx @wordpress/create-block to 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 a deprecated entry before shipping save changes.
  • For anything that needs live data (post queries, user info, ACF fields), always build a dynamic block with render.php — static save() 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.js and save.js as thin as possible — move complex logic into custom hooks or helper functions in a utils/ directory.
  • Use @wordpress/scripts (the wp-scripts CLI) as your build tool — it handles webpack, ESLint, Jest, and TypeScript support with zero config.
Helpful Resources & Links