Custom Gutenberg blocks let you extend WordPress’s block editor with editor UI components tailored to your site’s exact needs — instead of reaching for a page builder plugin that adds weight you don’t need. They’re the right tool when you need a repeatable content pattern (a testimonial, a pricing card, a call-to-action) that the default block library doesn’t cover.
This guide uses the current WordPress block development stack: block.json for block metadata (the standard since WordPress 5.8), useBlockProps() for block wrapper attributes (required since WordPress 5.6), and @wordpress/scripts for the build toolchain. Code that skips these — like examples using category: 'common' (removed in WordPress 6.0) or no useBlockProps() — will produce validation errors in modern WordPress.
Prerequisites
- Node.js 18+ and npm installed
- A local WordPress development environment (Local, Lando, or wp-env)
- Basic JavaScript (ES6+) and JSX/React familiarity
- PHP knowledge for the plugin registration file
If you’d rather scaffold the full block boilerplate automatically rather than build from scratch, @wordpress/create-block does it in one command: npx @wordpress/create-block@latest my-block. This guide walks through the manual approach so you understand what each file does.
Step 1: Create the plugin folder and install dependencies
Gutenberg blocks live inside WordPress plugins (or themes). Create a plugin folder inside wp-content/plugins/:
mkdir wp-content/plugins/my-custom-block
cd wp-content/plugins/my-custom-block
npm init -y
Install @wordpress/scripts — the official WordPress build toolchain that wraps webpack, Babel, and the WordPress package aliases:
npm install --save-dev @wordpress/scripts
Add build commands to package.json:
{
"scripts": {
"build": "wp-scripts build",
"start": "wp-scripts start"
}
}
npm start runs in watch mode (rebuilds on save, useful during development). npm run build produces the production output in build/. The plugin setup for WordPress is similar to the general plugin scaffolding covered in the WordPress plugin with Node.js guide.
Step 2: Create block.json
block.json is the block’s metadata file — the current standard since WordPress 5.8. It replaces passing options directly to registerBlockType(). Create src/block.json:
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "my-plugin/call-to-action",
"version": "1.0.0",
"title": "Call to Action",
"category": "text",
"icon": "megaphone",
"description": "A custom call-to-action block with editable heading and body text.",
"attributes": {
"heading": {
"type": "string",
"source": "html",
"selector": "h2"
},
"body": {
"type": "string",
"source": "html",
"selector": "p"
}
},
"editorScript": "file:./index.js",
"style": "file:./style-index.css",
"editorStyle": "file:./index.css"
}
Key points:
apiVersion: 3— current as of WordPress 6.3. Always specify this.category: "text"— valid in 2026. The"common"category was removed in WordPress 6.0 and will silently fail (the block appears under a generic “uncategorized” group). Valid categories:text,media,design,widgets,theme,embed.- Attributes in block.json — defining attributes here (rather than in
registerBlockType) is the modern approach. WordPress reads them at registration time.
Step 3: Register the block in PHP
Create the main plugin file my-custom-block.php:
<?php
/**
* Plugin Name: My Custom Block
* Description: A custom call-to-action Gutenberg block.
* Version: 1.0.0
* Requires at least: 6.3
*/
function my_custom_block_register() {
register_block_type( __DIR__ . '/build' );
}
add_action( 'init', 'my_custom_block_register' );
register_block_type( __DIR__ . '/build' ) reads block.json from the build/ directory and registers the block, scripts, and styles automatically. You don’t pass any options — block.json has everything WordPress needs.
Step 4: Write the block’s JavaScript (edit and save)
Create src/index.js:
import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps, RichText } from '@wordpress/block-editor';
import metadata from './block.json';
registerBlockType( metadata.name, {
edit: function Edit( { attributes, setAttributes } ) {
const blockProps = useBlockProps( {
className: 'my-cta-block',
} );
return (
<div { ...blockProps }>
<RichText
tagName="h2"
value={ attributes.heading }
onChange={ ( heading ) => setAttributes( { heading } ) }
placeholder="Enter heading..."
/>
<RichText
tagName="p"
value={ attributes.body }
onChange={ ( body ) => setAttributes( { body } ) }
placeholder="Enter body text..."
/>
</div>
);
},
save: function Save( { attributes } ) {
const blockProps = useBlockProps.save( {
className: 'my-cta-block',
} );
return (
<div { ...blockProps }>
<RichText.Content tagName="h2" value={ attributes.heading } />
<RichText.Content tagName="p" value={ attributes.body } />
</div>
);
},
} );
useBlockProps() in the edit function and useBlockProps.save() in the save function are both required. They inject the block’s wrapper attributes (class, data-*, and any attributes WordPress adds for block identification). Blocks without useBlockProps produce a block validation error in the editor — WordPress compares the saved markup against what save() would generate now, and the mismatch triggers the error.
Notice import metadata from './block.json': passing metadata.name to registerBlockType instead of a hardcoded string means the block name stays in sync with block.json automatically. Using React in WordPress blocks is the same JSX/hook model as any other React app — the WordPress theme with React guide covers the broader React-in-WordPress environment in detail.
Step 5: Add InspectorControls for block settings
InspectorControls lets you add settings to the block’s sidebar panel in the editor — toggle switches, colour pickers, text inputs, and other controls that affect the block’s appearance or behaviour without touching the content canvas.
import { InspectorControls, useBlockProps } from '@wordpress/block-editor';
import { PanelBody, ToggleControl } from '@wordpress/components';
// Inside your edit function:
edit: function Edit( { attributes, setAttributes } ) {
const blockProps = useBlockProps();
const { showButton } = attributes;
return (
<>
<InspectorControls>
<PanelBody title="Block Settings">
<ToggleControl
label="Show button"
checked={ showButton }
onChange={ ( val ) => setAttributes( { showButton: val } ) }
/>
</PanelBody>
</InspectorControls>
<div { ...blockProps }>
{ /* block canvas content here */ }
</div>
</>
);
}
Add the corresponding showButton attribute to block.json: "showButton": { "type": "boolean", "default": true }.
Step 6: Build and test the block
Start the development build watcher:
npm start
This compiles src/index.js → build/index.js and watches for changes. Activate your plugin in the WordPress admin (Plugins → Activate), then open a post or page in the block editor and search for your block name. If the block doesn’t appear, check the browser console for JavaScript errors and the PHP error log for registration failures.
When you’re ready to ship:
npm run build
This produces minified production output in build/. Only the build/ directory (and the plugin PHP file) need to be on the server — src/, node_modules/, and package.json stay off production.
Block validation errors: what they mean and how to fix them
If you edit the save() function after a post has been saved with the old version, the editor will show a block validation error — the stored HTML no longer matches what save() produces now. Three ways to handle this:
- Deprecations — the correct approach for blocks in production. Define the old
save()as a deprecated version so WordPress can still parse existing posts using the old markup. See the Block Deprecation docs. - Dynamic blocks — move rendering to PHP via a
render_callbackinblock.json, so thesave()function returnsnulland there’s no stored HTML to validate against. - During development only — delete the blocks from existing posts and re-insert them after changing
save().
Custom blocks pair naturally with Full Site Editing — once your block is working, it can be used in block templates and template parts across a block theme. The WordPress Full Site Editing guide covers the block theme layer. For reusable editor patterns built from multiple blocks (rather than custom block types), see block patterns vs InnerBlocks — they solve a different but related problem. The theme context for custom blocks is covered in the high-performance block themes guide, and for the full theme-from-scratch approach, see how to build a custom WordPress theme.


