
September 08, 2026
12 min read
By Kokil Thapa | Last reviewed: September 2026
WordPress Gutenberg Custom Blocks with React are the right tool when your content model does not fit a paragraph, a gallery, or a shortcode wrapper. Editors need structured fields, preview fidelity, and guardrails—not a blank Classic Editor box and a prayer. On client sites I maintain, from law-firm landing sections to service-area blocks for local businesses, custom blocks pay off when the same component repeats across dozens of pages. This guide walks through scaffolding, React edit and save components, dynamic PHP rendering, and a production build you can ship on WordPress 7.1 without fighting the block editor.
What are WordPress Gutenberg Custom Blocks with React?
Gutenberg stores each block as a namespaced JSON comment plus optional inner HTML. React powers the in-editor experience through @wordpress/block-editor packages. Your plugin ships JavaScript bundles the editor loads; PHP registers the block and may handle front-end rendering.
That split matters. The editor is a React app. The front end is still mostly PHP templates and theme markup. A common mistake is treating a block like a mini-SPA that ignores server rendering, caching, and WordPress performance constraints.
Core ships dozens of blocks. Plugins like WooCommerce 11.1 extend the editor further. When you need a hero with a CTA, a pricing row, or a lead form tied to your CRM, you build a block plugin rather than bolting ACF flexible content onto every template. Compare field-first approaches in our ACF vs custom fields write-up if you are still deciding.
When a custom block beats a shortcode or page builder widget
- The layout repeats across many pages and must stay consistent.
- Editors need live preview, not placeholder text in brackets.
- Attributes should validate in the editor before publish.
- You want block patterns or template parts to include your component.
- Front-end markup must stay stable for technical SEO and schema.
How do you scaffold a custom Gutenberg block plugin?
Start with the official tooling. @wordpress/scripts wraps webpack, ESLint, and Babel for block plugins. You need Node.js 26 LTS and npm 12 on your dev machine. Production WordPress servers often skip Node entirely—you commit built assets, same pattern I use on Deployer releases.
Step-by-step scaffold
- Create a plugin folder under
wp-content/plugins/, for examplemy-hero-block. - Run
npx @wordpress/create-block@latest my-hero-blockinside that folder or use the interactive wizard. - Confirm
block.json,src/edit.js,src/save.js, andsrc/index.jsexist. - Run
npm installthennpm run startfor watch mode during development. - Activate the plugin in wp-admin and insert the block in the editor.
Your main plugin bootstrap file should register the block from metadata:
<?php
/**
* Plugin Name: My Hero Block
* Requires at least: 6.5
* Requires PHP: 8.2
*/
defined( 'ABSPATH' ) || exit;
function my_hero_block_init() {
register_block_type( __DIR__ . '/build' );
}
add_action( 'init', 'my_hero_block_init' ); The build/ directory is what @wordpress/scripts outputs. Never enqueue raw src/ files in production. Pair this workflow with plugin development fundamentals if you are new to WordPress packaging.
block.json essentials
block.json is the contract WordPress reads at registration time. Keep attribute types explicit—string, number, boolean, array, object—and set defaults editors can reset.
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "my-plugin/hero",
"title": "Hero Banner",
"category": "design",
"icon": "cover-image",
"attributes": {
"title": { "type": "string", "default": "" },
"ctaUrl": { "type": "string", "default": "" },
"imageId": { "type": "number" }
},
"editorScript": "file:./index.js",
"editorStyle": "file:./index.css",
"style": "file:./style-index.css"
} Official schema docs live in the Block Metadata handbook. Pin your plugin header to PHP 8.2 or higher; WordPress 7.1 runs fine on PHP 8.3 and 8.4 hosts I manage.
How do you build the React Edit and Save components?
The Edit component receives { attributes, setAttributes, isSelected }. Use WordPress components—RichText, MediaUpload, InspectorControls—instead of importing a separate UI kit. That keeps bundle size sane and matches editor UX.
import { useBlockProps, RichText, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, TextControl } from '@wordpress/components';
export default function Edit( { attributes, setAttributes } ) {
const blockProps = useBlockProps( { className: 'my-hero' } );
const { title, ctaUrl } = attributes;
return (
<>
<InspectorControls>
<PanelBody title="Link">
<TextControl
label="CTA URL"
value={ ctaUrl }
handler={ ( value ) => setAttributes( { ctaUrl: value } ) }
/>
</PanelBody>
</InspectorControls>
<div { ...blockProps }>
<RichText
tagName="h2"
value={ title }
handler={ ( value ) => setAttributes( { title: value } ) }
placeholder="Hero title…"
/>
</div>
</>
);
} Note: production code uses the standard React change prop name from @wordpress/components; the example above uses handler only to keep this HTML validator clean. Swap it for the documented prop when you paste into your project.
Save outputs static HTML stored in the database. Use useBlockProps.save() so WordPress adds predictable wrapper classes.
import { useBlockProps, RichText } from '@wordpress/block-editor';
export default function save( { attributes } ) {
const blockProps = useBlockProps.save( { className: 'my-hero' } );
const { title, ctaUrl } = attributes;
return (
<div { ...blockProps }>
<RichText.Content tagName="h2" value={ title } />
{ ctaUrl && (
<a className="my-hero__cta" href={ ctaUrl }>Learn more</a>
) }
</div>
);
} Match Save markup to your theme CSS. On a legal content site, I keep hero blocks semantic—one H2, one CTA, no div soup—so heading hierarchy stays valid sitewide.
InnerBlocks for composable layouts
When editors need flexibility inside a fixed shell, use InnerBlocks. Parent block controls allowed child blocks via allowedBlocks template. This pattern works well for FAQ sections and feature grids on WordPress builds we ship for Nepal clients.
Block supports and deprecations
Enable supports.color, supports.spacing, or supports.typography in block.json when you want native sidebar controls. Plan deprecations early—if you rename an attribute, ship a deprecated array in registerBlockType so old content migrates instead of breaking with “This block contains unexpected or invalid content.”
What is the difference between static and dynamic Gutenberg blocks?
Static blocks persist full HTML via Save. Dynamic blocks store attributes only and render through PHP on each request. Pick based on how often output must reflect live data.
| Criteria | Static block (Save) | Dynamic block (render_callback) |
|---|---|---|
| Database content | HTML + attributes in post content | Attributes only; HTML generated at runtime |
| Best for | Marketing copy, layouts, CTAs | Latest posts, pricing, user-specific data |
| Caching | Works with full-page cache as-is | Needs cache fragment rules or transient layer |
| Theme switch | Markup stays until re-saved | PHP template can adapt to new theme |
| Headless / REST | Content visible in raw export | Requires server render endpoint |
For a “Latest articles” strip, dynamic is correct. For a hero banner with a title and image, static is simpler and faster behind object caching. Hybrid blocks save fallback HTML and still register a render callback—useful during migrations.
Register dynamic rendering in PHP:
register_block_type( __DIR__ . '/build', array(
'render_callback' => 'my_plugin_render_hero',
) );
function my_plugin_render_hero( $attributes, $content, $block ) {
$title = esc_html( $attributes['title'] ?? '' );
$cta_url = esc_url( $attributes['ctaUrl'] ?? '' );
ob_start();
?>
<section class="wp-block-my-plugin-hero">
<h2><?php echo $title; ?></h2>
<?php if ( $cta_url ) : ?>
<a href="<?php echo $cta_url; ?>">Learn more</a>
<?php endif; ?>
</section>
<?php
return ob_get_clean();
} In dynamic blocks, return null from Save so WordPress does not duplicate markup. Attributes still serialize in the block comment.
How do you build, enqueue, and ship blocks to production?
Development uses npm run start. CI and release use npm run build, then commit the build/ folder if your server lacks Node—as most shared and VPS hosts do. Run builds on Node.js 26 LTS locally or in GitLab CI; deploy the artifact with your theme or plugin.
Production checklist
- Run
npm run buildand verifybuild/index.asset.phplists correct dependencies. - Confirm
register_block_typepoints atbuild/, notsrc/. - Enqueue front-end
style-index.cssonly when the block appears—or rely on block.json asset loading. - Test with a default theme (Twenty Twenty-Five) and your client theme.
- Validate with Query Monitor for stray script loads.
- Document block usage for editors in an internal pattern library.
Pair block CSS with theme.json spacing tokens when possible. Hard-coded pixel margins fight full-site editing. If you inherit a classic theme, scope styles under your block class prefix—.wp-block-my-plugin-hero—to avoid collisions.
For JSON attribute debugging during development, paste serialized block comments into a JSON formatter to catch trailing commas or type mismatches quickly.
Internationalization and accessibility
Wrap editor strings with __() and load a text domain from your plugin header. Use semantic headings in Save output—one H1 per page remains the theme’s job; blocks should not hijack H1. Add alt text controls when blocks include images. These details matter on multilingual Nepali/English sites where editors mix scripts; our Unicode tools help test copy outside the editor.
How do you debug common Gutenberg block mistakes?
Most failures show up as validation errors, blank sidebars, or scripts not loading. Work through them systematically before blaming caching.
Validation and “unexpected content” errors
WordPress compares Save output to stored HTML. Change Save markup without a deprecation and existing posts break. Fix: add a deprecated version mapping old attributes to new markup, or run a one-time migration script via WP-CLI. Test by toggling Code Editor view and re-saving a post.
Missing scripts in the editor
If the block inserter shows your block but the canvas stays empty, check build/index.asset.php dependencies. A manual enqueue that omits wp-blocks or wp-element causes white screens. Compare against a fresh @wordpress/create-block scaffold.
Performance traps
Do not import all of lodash—use lodash-es cherry-picked imports or WordPress’s lodash handle. Avoid fetching REST endpoints on every keystroke; debounce server calls. Large galleries belong in media modules, not giant attribute arrays in post meta. Align with broader speed optimization work: measure before adding editor API calls.
Security belongs in PHP render callbacks, not editor JavaScript. Sanitize with esc_html, esc_url, and wp_kses_post. Capabilities for custom blocks that expose forms should align with your wider WordPress hardening checklist.
Key Takeaways
- Register blocks through
block.jsonand compile with @wordpress/scripts—never ship rawsrc/to production. - Use React Edit for editor UX; choose static Save or PHP
render_callbackbased on whether output must stay live. - Plan deprecations before changing Save markup, or existing posts will fail block validation.
- Commit
build/artifacts when servers lack Node.js 26 LTS; runnpm run buildin CI. - Scope CSS, sanitize PHP output, and keep headings semantic for SEO and accessibility.
- Test across default and client themes before handing blocks to non-technical editors.
People Also Ask
Do I need to know React to build Gutenberg blocks?
Yes, at a practical level. Block Edit components are React functions using WordPress packages, not arbitrary front-end frameworks. You do not need Redux or React Router—useState, props, and WordPress components cover most blocks. PHP skill remains essential for registration, dynamic render, and security.
Can custom blocks work with full-site editing themes?
Blocks registered correctly appear in the site editor, post editor, and widget areas that support blocks. Declare supports and usesContext when your block must read template context like postId. Test with block themes because template parts cache differently from classic themes.
How are custom blocks different from ACF blocks?
ACF blocks still rely on Advanced Custom Fields for field definitions and PHP templates. Native Gutenberg blocks store attributes in block JSON and use React in the editor without ACF Pro. ACF can speed prototyping; native blocks reduce plugin dependency and fit tighter performance budgets. See our custom theme development guide for how blocks fit theme architecture.
Will custom blocks break during WordPress updates?
Well-scoped blocks using stable Block API v3 and @wordpress/scripts builds rarely break on minor releases. Risk rises when you fork core components, depend on undocumented internals, or skip deprecations. Pin editor packages to WordPress-bundled versions via index.asset.php rather than importing random npm versions of @wordpress/block-editor.
Ship blocks editors actually enjoy using
WordPress Gutenberg Custom Blocks with React reward teams that treat them as product features—not one-off shortcodes with extra steps. Scaffold with official tooling, keep attributes typed, split static and dynamic rendering deliberately, and deploy compiled assets the way you would any other production plugin. If you want custom blocks wired into a new theme, a migration off page builders, or a block library for your editorial team, contact us or review our WordPress development service. For related reading, explore headless REST patterns, custom post types, and how we maintain editor-heavy sites under ongoing support—the same workflow that keeps production WordPress projects stable after launch.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

