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:
| Field | Type | Attribute name |
|---|---|---|
| Author name | Text | authorName |
| Quote | Textarea | quoteText |
| Show photo | Toggle | showPhoto |
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 + build | Core PHP-only | ACF Blocks | FanCoolo WP | |
|---|---|---|---|---|
| Language | React/JSX + PHP | PHP | PHP | PHP (+ optional JS) |
| Build step | Yes (npm) | No | No | No |
| Fields | Hand-written React controls | Generated for string, number, integer and boolean attributes | ACF field groups | Visual attributes manager, native controls |
| Needs | Node.js | WordPress 7.0+ | ACF Pro | FanCoolo WP |
| Changing the markup later | Existing posts keep the old saved HTML until each is re-saved | Every page updates at once | Every page updates at once | Every page updates at once |
| Best for | Complex editor UIs, core-like blocks | Simple server-rendered blocks with a few settings | Sites already built around ACF | Client 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
- PHP-only block registration, Miguel Fonseca, Make WordPress Core, 3 March 2026; the limits on attribute types and nested blocks are from the author’s replies in the comments, 4 March 2026 (read 2 October 2026)
- register_block_type(), WordPress Code Reference (read 2 October 2026)
- Deprecation, Block Editor Handbook (read 2 October 2026)
- Attributes Manager and Inner Blocks, FanCoolo WP documentation (read 2 October 2026)
Tested with WordPress 7.1.2: the Option 2 example registers and renders. The generated sidebar controls were not tested in the editor.
Related guides
- How to Build a Gutenberg Block That Shows ACF Fields: Show Advanced Custom Fields data in the block editor.
- How to Build a Dynamic Gutenberg Block That Lists Posts: List posts with WP_Query in a custom PHP block.
- How to Build Reusable Components Across Gutenberg Blocks: Write a button or card once, use it in every block.
- How to Use SCSS in Custom Gutenberg Blocks: Variables, nesting and shared partials for blocks.
Browse all WordPress tutorials.
