Building custom Gutenberg blocks with React is the most direct way to give content editors exactly the components a design calls for β structured, validated, and impossible to break β instead of asking them to reconstruct layouts from generic blocks or shortcodes. The development experience has matured enormously: one scaffold command produces a working, build-configured block, and a single JSON file describes it to both PHP and JavaScript. This walkthrough builds a real block β a testimonial card β and covers the decisions that matter along the way.
Scaffolding: one command to a working block
The official @wordpress/create-block package generates a complete block plugin with modern build tooling (@wordpress/scripts, which wraps webpack, Babel, and the linters) already configured:

npx @wordpress/create-block@latest acme-testimonial
cd acme-testimonial
npm start Activate the generated plugin and the block is already insertable. The files that matter live in src/: block.json (the manifest), edit.js (the React component rendered in the editor), save.js (the serialized output), plus stylesheets and the registration entry point. npm start rebuilds on save; npm run build produces the production assets.
block.json: the single source of truth
Everything about a modern block is declared in block.json β name, category, attributes, supported features, and asset files. WordPress reads it on the PHP side via register_block_type() pointed at the file, and the build tooling reads it on the JavaScript side. A trimmed manifest for our testimonial block:
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "acme/testimonial",
"title": "Testimonial",
"category": "text",
"attributes": {
"quote": { "type": "string", "source": "html", "selector": "blockquote" },
"author": { "type": "string" },
"rating": { "type": "number", "default": 5 }
},
"supports": {
"color": { "background": true, "text": true },
"spacing": { "padding": true },
"align": [ "wide" ]
},
"editorScript": "file:./index.js",
"style": "file:./style-index.css"
} Two parts deserve attention. Attributes are the block's data model: each one has a type, optionally a default, and optionally a source describing where it lives in the saved markup. Supports is free functionality β declaring color, spacing, and typography supports gives your block the same design controls core blocks have, wired to the theme's palette and theme.json, with zero code written by you. Always exhaust supports before building custom controls.
The edit component: React, but WordPress-flavored
The edit.js component is ordinary React, but the WordPress component library does the heavy lifting. Three packages appear in nearly every block: @wordpress/block-editor for editor-aware components, @wordpress/components for generic UI, and @wordpress/i18n for translatable strings.

import { __ } from '@wordpress/i18n';
import {
useBlockProps,
RichText,
InspectorControls,
} from '@wordpress/block-editor';
import { PanelBody, RangeControl } from '@wordpress/components';
export default function Edit( { attributes, setAttributes } ) {
const { quote, author, rating } = attributes;
return (
<>
<InspectorControls>
<PanelBody title={ __( 'Settings', 'acme-testimonial' ) }>
<RangeControl
label={ __( 'Rating', 'acme-testimonial' ) }
value={ rating }
onChange={ ( value ) => setAttributes( { rating: value } ) }
min={ 1 }
max={ 5 }
/>
</PanelBody>
</InspectorControls>
<figure { ...useBlockProps() }>
<RichText
tagName="blockquote"
value={ quote }
onChange={ ( value ) => setAttributes( { quote: value } ) }
placeholder={ __( 'Add the testimonialβ¦', 'acme-testimonial' ) }
/>
<RichText
tagName="figcaption"
value={ author }
onChange={ ( value ) => setAttributes( { author: value } ) }
placeholder={ __( 'Author name', 'acme-testimonial' ) }
/>
</figure>
</>
);
} The idioms to internalize: useBlockProps() must spread onto the wrapper element (it carries the classes, styles, and supports plumbing); RichText gives editors inline formatting within your structure; InspectorControls renders into the sidebar; and all state flows through setAttributes, which is what makes undo, redo, and autosave work for free. There is no local component state for content β attributes are the state.
The big decision: static save or dynamic render
Every block chooses how its front-end markup is produced, and this is the most consequential architectural decision.
Static blocks
A static block implements save.js, and the resulting HTML is stored in post content. This is ideal for purely presentational blocks like our testimonial: no server work at render time, and the content is portable. The cost is deprecations β if a later version changes the saved markup, you must ship a deprecation entry so existing content migrates cleanly instead of throwing block validation errors. Plan for this from version one; it is the classic hard lesson of block development.
Dynamic blocks
A dynamic block saves little or nothing and renders on the server, either through a render.php file referenced in block.json or a render callback. Choose dynamic whenever output depends on data that changes independently of the post: query results, current user, time, or external APIs. Dynamic blocks never face markup deprecation issues, at the cost of PHP work on every uncached render β and if the block needs front-end interactivity, a dynamic block pairs naturally with the Interactivity API and a view.js file.
A useful rule: content blocks are static, data blocks are dynamic. When in doubt, dynamic is more forgiving to iterate on.
Polish that separates professional blocks
- Variations and patterns. Register block variations for common configurations, and ship patterns composing your block with core blocks, so editors start from something good.
- InnerBlocks. For container-style blocks,
InnerBlockswith a template and anallowedBlockslist lets editors nest content within your structure β usually better than inventing custom fields for everything. - Editor parity. Make the editor view genuinely resemble the front end by sharing styles; editors trust blocks that look right while editing.
- Accessibility. Semantic markup in
save.js, labeled controls, and keyboard-reachable interactions are requirements, not extras. - i18n. Wrap every string with
__()and load script translations; block UI is user-facing UI.
The bottom line
Custom block development in 2026 is approachable in a way it simply was not in the early Gutenberg years: create-block removes the tooling barrier, block.json removes the registration boilerplate, and supports removes most custom-control code. The craft now lives in the decisions β a clean attribute model, the static-versus-dynamic call, and deprecation discipline. Get those right and blocks become what they promised to be: durable, editor-friendly components that encode your design system directly into the editing experience.

Related Service
π» Web Development
Custom websites and web applications built with PHP, Laravel, WordPress, and React by a Chandigarh & Mohali based web developer β fast, secure, scalable, and trusted by clients across India and worldwide.
Explore Web Development →
Reviews & Comments
Reviews are moderated and appear after approval.
No reviews yet β be the first to share your thoughts.
Leave a Review