Custom Gutenberg blocks,
without React or a build step.
A custom block needs four things: a name, some fields, an interface for editing them and markup for the front end. WordPress makes you supply all four. GutenFields asks for the first two and generates the third, leaving you one PHP template to write. This page builds a complete block from nothing.
Why a simple block takes so many files.
Nothing about a testimonial is complicated. Registering one as a native block is, because the block API is built for blocks that need arbitrary JavaScript — and it charges every block that price, including the ones that never needed it.
| A block needs | Natively | With GutenFields |
|---|---|---|
| A name and metadata | A block.json per block, plus registration that reads it |
Two keys in the field definition |
| Somewhere to keep the data | An attributes map you write and keep in step with the UI |
Derived from the fields |
| An editing interface | A React edit component, so a build step and a toolchain |
Generated from the field types |
| Front-end markup | A render.php, or a save function |
A PHP template in your theme — the one file you write |
The interesting column is the third row. A generated interface is possible because a
field type already implies its control: a toggle is a switch, a
select is a dropdown, an image opens the media library. Write
those out as React components thirty times and you have thirty components to maintain;
describe them once as data and there is nothing to maintain at all.
A testimonial block, in two files.
This is the whole job. The first file goes anywhere your theme already loads —
functions.php, an inc/blocks.php it includes, or an
mu-plugin. The second is the markup.
1. Describe the fields
add_action( 'init', function () {
if ( ! function_exists( 'gutenfields_register_block' ) ) {
return;
}
gutenfields_register_block( [
'name' => 'testimonial', // → block gutenfields/testimonial
'title' => 'Testimonial',
'fields' => [
[ 'name' => 'quote', 'label' => 'Quote', 'type' => 'rich' ],
[ 'name' => 'author', 'label' => 'Author', 'type' => 'text' ],
[ 'name' => 'photo', 'label' => 'Photo', 'type' => 'image' ],
[ 'name' => 'rated', 'label' => 'Show a rating', 'type' => 'toggle' ],
[
'name' => 'rating',
'label' => 'Rating',
'type' => 'range',
'min' => 1,
'max' => 5,
// Only offered once the toggle above is on.
'conditions' => [
[ 'field' => 'rated', 'operator' => '==', 'value' => '1' ],
],
],
],
] );
}, 5 ); // before the priority-10 init where blocks are booted
That is the block registered. Reload the editor and Testimonial is in the inserter with a rich-text quote, an author line, a media picker, a switch and a slider that appears only when the switch is on — none of which you have written an interface for.
2. Write the markup
<figure <?php echo get_block_wrapper_attributes(); ?>>
<blockquote>
<?php echo wp_kses_post( gf_field( 'quote' ) ); ?>
</blockquote>
<?php if ( $photo = gf_image( 'photo' ) ) : ?>
<img src="<?php echo esc_url( $photo['url'] ); ?>"
alt="<?php echo esc_attr( $photo['alt'] ); ?>" />
<?php endif; ?>
<figcaption><?php gf_the_field( 'author' ); ?></figcaption>
<?php // A hidden field keeps its value, so branch on the toggle here. ?>
<?php if ( gf_field( 'rated' ) ) : ?>
<p class="stars"><?php echo str_repeat( '★', (int) gf_field( 'rating' ) ); ?></p>
<?php endif; ?>
</figure>
You can skip the second file to begin with
A block with no template auto-renders one BEM-classed element per field, so it is
usable in the editor the moment it is registered. Drop the template in whenever you
are ready to take over — or run wp gutenfields scaffold testimonial and
have a starter generated from the fields.
Not every block deserves a commit.
A block that a theme depends on belongs in the theme. A block someone asked for on a Thursday afternoon does not, and making a developer open an editor for it is how the list of blocks stops growing. All three routes produce the same kind of block.
Config-as-code
gutenfields_register_block() in your theme. Reviewed in a pull request,
deployed with the theme, identical on every environment. This is the one to reach
for by default.
The admin field builder
A three-pane screen in wp-admin. Blocks built here live in the database, and Export PHP turns one into the code above the moment it has earned a place in the repository.
JSON import
A portable file matching the published schema — how definitions move between environments, and the format an AI assistant writes when you ask it for a block.
A code block and a saved block that share a name are not a conflict: the code version wins, every time. Prototyping in the builder and then committing the export is a supported path rather than a thing you get away with.
Rows, and rows of different shapes.
Most real sections are lists — three features, five logos, an arbitrary stack of page sections. Two field types cover that, and both are in the paid edition.
Repeater Pro
A field whose value is a list of rows, each with the same subfields. The editor gets
add, reorder and delete controls for free; the template loops over the rows and reads
each one with the gf_sub_* helpers.
Flexible content Pro
A list whose rows may each be a different named layout — a hero, then a pull quote, then a call to action — with its own subfields. Loop it and branch on the layout name the loop hands you.
<?php // A repeater: every row has the same subfields. ?>
<?php foreach ( gf_repeater( 'features' ) as $row ) : ?>
<h3><?php gf_the_sub_field( 'title' ); ?></h3>
<p><?php gf_the_sub_field( 'body' ); ?></p>
<?php endforeach; ?>
<?php // Flexible content: each row names its own layout. ?>
<?php foreach ( gf_flexible( 'sections' ) as $layout => $row ) :
get_template_part( 'parts/section', $layout );
endforeach; ?>
Containers nest five levels deep, which is a recursion guard rather than a target — the manual explains why two or three is usually the real limit. Options pages, for the settings that belong to the site rather than to any one page, are in the same tier.
Next to the APIs WordPress already gives you.
This is not a replacement for the block editor, and there are jobs it is the wrong tool for. Worth knowing which is which before you commit a theme to it.
block.json and React
Still the right answer for a block that needs behaviour in the editor — a canvas, a custom toolbar, a live-rendering preview. If the block is a form the editor fills in, that machinery is overhead you are paying for nothing.
The Block Bindings API
Added in WordPress 6.5, it connects a core block's attribute to a source such as post meta — a paragraph that shows a custom field. It binds existing blocks; it does not give you a new block with a designed field set. Different job.
Patterns and synced patterns
If the thing you want is a preset arrangement of core blocks, a pattern is simpler than any plugin and costs nothing. Reach for fields when an editor should fill in values, not arrange markup.
ACF Blocks
The nearest neighbour, and the same idea: fields plus a PHP template. The differences are what a field group attaches to and where its values are stored — set out in full here.
Post meta
GutenFields keeps values in the block's attributes, in post content. That makes a block portable between posts, and it means you cannot query on the values. Data the post owns still belongs in meta.
Page builders
There is no drag-and-drop layout canvas here and no styling UI. You get fields, a block and a template; everything about how it looks is your own CSS.
Building blocks, answered.
Q. Can I really create a custom Gutenberg block without React?
Q. Is there a build step, npm install or bundler?
Q. Where is a block's data stored?
Q. What happens to my content if I deactivate the plugin?
Q. Does the editor preview my template?
Q. How many field types are there, and which need a licence?
Q. Can an AI assistant write these definitions for me?
Register your first block in about five minutes.
The free edition is a finished plugin, not a trial: nineteen field types, conditional logic, the builder, templates and the CLI, with no expiry. Paste the definition above into your theme and reload the editor.