How to Create a Custom Gutenberg Block Without React

You can build a Gutenberg block without React in three ways: register it in PHP with WordPress 7.0’s autoRegister flag, render it with ACF Blocks, or let a plugin generate it from a PHP template and fields.

Custom blocks are the most useful thing you can add to a WordPress site: a testimonial, a pricing card, a feature grid that editors can drop onto any page without breaking the design. For years, building one meant React, JSX, npm and a build step. This guide shows the four realistic ways to build a custom Gutenberg block, React included, and how to build one with PHP only.

What a custom Gutenberg block is

A custom block is a new block type that appears in the block inserter next to Paragraph, Image and Heading. Every time an editor inserts it, they get the same markup and styles, and they only change the content you allow them to change. Every block has a configuration (its name and attributes), an editor side and a front end. For what each side does, see custom Gutenberg blocks: React or PHP?

Option 1: React and a build step

WordPress’s own tooling scaffolds a block with one command:

npx @wordpress/create-block@latest my-testimonial
cd my-testimonial
npm start

You get a plugin with block.json, an edit.js written in React/JSX, a save.js (or a render.php for dynamic blocks) and a webpack build. Every change goes through npm start or npm run build.

This is the most flexible approach, and it’s what core blocks use. It is also a lot of machinery if what you want is “a card with a title, text and a button”: you need Node.js on every machine that edits the block, you ship build output, and the editor preview is a separate React component you have to keep in sync with the front end.

A second cost shows up later. A static React block saves its HTML into every post that uses it. When you change the block’s markup, those posts keep the old HTML, so visitors don’t see the change. In the editor the old blocks show “This block contains unexpected or invalid content” unless you write a deprecation for each earlier version, and you still have to open and re-save every page that uses the block. A block rendered by PHP has no saved HTML: change the template once and every page shows the new output on its next load.

Option 2: PHP-only blocks in WordPress core (7.0+)

Since WordPress 7.0 you can register a block from PHP alone: call register_block_type() with autoRegister turned on under supports, and a render_callback for the output. There is no JavaScript file and no build step.

add_action( 'init', function () {
    register_block_type( 'my-plugin/testimonial', array(
        'title'           => 'Testimonial',
        'attributes'      => array(
            'authorName' => array( 'type' => 'string', 'default' => '' ),
            'quoteText'  => array( 'type' => 'string', 'default' => '' ),
            'showPhoto'  => array( 'type' => 'boolean', 'default' => true ),
        ),
        'supports'        => array( 'autoRegister' => true ),
        'render_callback' => function ( $attributes ) {
            return sprintf(
                '<figure %s><blockquote>%s</blockquote><figcaption>%s</figcaption></figure>',
                get_block_wrapper_attributes( array( 'class' => 'testimonial' ) ),
                esc_html( $attributes['quoteText'] ),
                esc_html( $attributes['authorName'] )
            );
        },
    ) );
} );

The editor generates the sidebar controls for you from string, number, integer and boolean attributes, and an enum becomes a dropdown.

A PHP-only block is not a smaller React block. It has no editor code of its own: WordPress builds the sidebar from its attributes and renders the block on the server.

The limits are in the dev note: this is “meant for blocks that only need server-side rendering and aren’t meant to be highly interactive”. Attributes can’t be read from the block’s HTML, so editors can’t type text directly in the canvas. Nested blocks (InnerBlocks) aren’t supported, and no controls are generated for other field types such as images.

Option 3: ACF Blocks

If you already use Advanced Custom Fields Pro, you can register a block in block.json with an acf section and render it with a PHP template. Fields come from an ACF field group. It avoids React, but the block depends on ACF Pro, the fields live in a separate field-group screen, and the editor preview is ACF’s rendering rather than native Gutenberg controls.

Option 4: PHP only, with FanCoolo WP

FanCoolo WP is the block builder we make at DPlugins, developed by Marko Krstić, for the blocks core’s PHP-only registration doesn’t cover. You write the output in PHP, the style in SCSS, and add fields with a visual Attributes Manager that includes image, date, color and link fields. Container blocks with InnerBlocks are a setting. FanCoolo generates the block.json for you, so the result is a real block type, registered like any core block.

Step 1: create the block

In WordPress, open FanCoolo and add a new block. Give it a name, for example “Testimonial”. That’s the name editors will see in the block inserter.

Step 2: add the fields editors can change

Open the Attributes tab and click + Add attribute for each field:

FieldTypeAttribute name
Author nameTextauthorName
QuoteTextareaquoteText
Show photoToggleshowPhoto

FanCoolo shows a PHP example under each attribute that you can copy straight into your code. The fields appear in the Gutenberg sidebar when the block is selected, using native WordPress components, so editors get the same experience as with core blocks.

Step 3: write the output in PHP

In the Content tab, write the block’s HTML. The values editors entered are in $attributes:

<?php
$author = $attributes['authorName'] ?? '';
$quote  = $attributes['quoteText'] ?? '';
?>
<figure <?php echo get_block_wrapper_attributes( [ 'class' => 'testimonial' ] ); ?>>
  <blockquote><?php echo esc_html( $quote ); ?></blockquote>
  <figcaption><?php echo esc_html( $author ); ?></figcaption>
</figure>

Because this is PHP, the same template can pull data from the database, loop over posts, or include Symbols: reusable PHP components (a button, a card) that you write once and use in many blocks.

Step 4: style it

Add SCSS in the Style tab. It’s compiled to CSS and loaded both in the editor and on the front end, so what editors see is what visitors get. Shared variables and mixins can live in SCSS Partials, and a partial marked as global is included in every block. If the editor needs something different (for example no animations while editing), use the Editor Style tab.

Step 5: add interactivity (optional)

Tabs, accordions and sliders need JavaScript on the front end. Put it in the View tab. It loads only on the front end, not in the editor. Turn on the Module toggle if you want to use the WordPress Interactivity API.

Save, and the block is in the inserter, ready to use on any page.

Which option should you pick?

React + buildCore PHP-onlyACF BlocksFanCoolo WP
LanguageReact/JSX + PHPPHPPHPPHP (+ optional JS)
Build stepYes (npm)NoNoNo
FieldsHand-written React controlsGenerated for string, number, integer and boolean attributesACF field groupsVisual attributes manager, native controls
NeedsNode.jsWordPress 7.0+ACF ProFanCoolo WP
Changing the markup laterExisting posts keep the old saved HTML until each is re-savedEvery page updates at onceEvery page updates at onceEvery page updates at once
Best forComplex editor UIs, core-like blocksSimple server-rendered blocks with a few settingsSites already built around ACFClient sites, agencies, PHP developers

If your blocks need a completely custom editing interface, the React route is worth it. If a block only needs a few text, number or on/off settings, core’s PHP-only registration may be all you need. FanCoolo WP covers what that leaves out: image, color, link and post-picker fields, nested blocks, SCSS and reusable Symbols. For the everyday blocks most sites need (cards, heroes, testimonials, feature grids, post lists), PHP is enough, and skipping the build step makes them faster to build and easier to hand over.

For a closer look at the trade-offs, see ACF blocks vs native Gutenberg blocks. Next steps: sidebar settings and nested blocks.

FAQ

Can Gutenberg blocks be written entirely in PHP?

Yes, since WordPress 7.0, for simple server-rendered blocks: register the block with autoRegister and a render_callback. Richer editing still needs JavaScript or a tool.

What can’t a PHP-only block do?

It has no text you type directly in the canvas, no nested blocks, and sidebar controls only for string, number, integer and boolean attributes (plus a dropdown for enum).

Is @wordpress/create-block required to make a block?

No. It is the scaffold for the React route. PHP-only registration, ACF Blocks and FanCoolo WP don’t use it.

Sources

Tested with WordPress 7.1.2: the Option 2 example registers and renders. The generated sidebar controls were not tested in the editor.

Build your first block with FanCoolo WP

Build your first block with FanCoolo WP

Native Gutenberg blocks in PHP: visual fields, SCSS, reusable symbols and hot reload. No React, no npm.

Related guides

Browse all WordPress tutorials.