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:

Scaffolding: one command to a working block β€” Building Custom Gutenberg Blocks with React: A Developer Walkthrough
Scaffolding: one command to a working block
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.

The edit component: React, but WordPress-flavored β€” Building Custom Gutenberg Blocks with React: A Developer Walkthrough
The edit component: React, but WordPress-flavored
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, InnerBlocks with a template and an allowedBlocks list 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.

Polish that separates professional blocks β€” Building Custom Gutenberg Blocks with React: A Developer Walkthrough
Polish that separates professional blocks

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 →
Share this article
X Facebook LinkedIn