Most Gutenberg blocks I build for clients are the same pattern: a handful of fields, maybe a repeater, a render function. But every time, you need @wordpress/scripts, a build step, JSX components, the whole thing. It always felt disproportionate.
So I wrote Define Blocks. It's a WordPress plugin that lets you register blocks entirely from PHP. You describe your fields with an array, tell it where they go, pass a render callback, done. The plugin generates the editor UI for you.
Here's the simplest possible block:
define_block_type( 'myplugin/notice', [
'title' => 'Notice',
'icon' => 'warning',
'category' => 'common',
'render' => function( $attributes ) {
$text = esc_html( $attributes['values']['text'] ?? '' );
return '<div class="notice-box">' . $text . '</div>';
},
'schema' => [
[
'scope' => 'content',
'fields' => [
'text' => [ 'type' => 'text', 'label' => 'Text' ],
],
],
],
]);
That's it. No build, no JSX, no block.json. The plugin takes care of registering the block type, generating the editor fields, and calling your render on the frontend.
When it gets a bit more interesting
The thing I actually needed for client work was repeaters with conditional visibility. Say you have a FAQ block where each item has an optional icon:
define_block_type( 'myplugin/faq', [
'title' => 'FAQ',
'icon' => 'editor-help',
'render' => 'myplugin_faq_render',
'schema' => [
[
'scope' => 'content',
'fields' => [
'items' => [
'type' => 'repeater',
'label' => 'Questions',
'fields' => [
'question' => [ 'type' => 'text', 'label' => 'Question' ],
'answer' => [ 'type' => 'richtext', 'label' => 'Answer' ],
'has_icon' => [ 'type' => 'checkbox', 'label' => 'Custom icon' ],
'icon' => [
'type' => 'media',
'label' => 'Icon',
'show' => 'has_icon',
],
],
],
],
],
],
]);
The show key accepts an expression syntax. You can do equality checks (type == "text"), boolean logic (a && b), negation (!field), comparisons (count > 0), even reference parent or root scope fields from inside a repeater (parent.layout, root.global_toggle). It covers most real-world cases without writing JS.
Scopes
Fields don't have to live in the content area. You can split them across the sidebar and toolbar:
'schema' => [
[
'scope' => 'content',
'fields' => [
'title' => [ 'type' => 'text', 'label' => 'Title' ],
'body' => [ 'type' => 'richtext', 'label' => 'Body' ],
],
],
[
'scope' => 'inspector',
'panel' => 'Layout',
'fields' => [
'columns' => [
'type' => 'select',
'label' => 'Columns',
'options' => [
[ 'value' => '1', 'label' => '1' ],
[ 'value' => '2', 'label' => '2' ],
[ 'value' => '3', 'label' => '3' ],
],
],
'background' => [ 'type' => 'color', 'label' => 'Background' ],
],
],
[
'scope' => 'toolbar',
'fields' => [
'alignment' => [
'type' => 'toolbar-select',
'options' => [
[ 'value' => 'left', 'icon' => 'align-left' ],
[ 'value' => 'center', 'icon' => 'align-center' ],
[ 'value' => 'right', 'icon' => 'align-right' ],
],
],
],
],
],
Each inspector scope with a different panel value creates a separate sidebar panel. There's also inspector-advanced for the Advanced section and toolbar-dropdown for grouped toolbar controls.
What it's not
It's not a page builder, it's not a no-code tool, it's not ACF. There's no admin UI, no field group editor, no database tables. You write PHP arrays and get blocks. If you already know how WordPress block registration works, the mental model is pretty much the same, just without the JS part.
The render callback works the same way as a standard render_callback in register_block_type. You get $attributes (your field values live in $attributes['values']) and you return HTML. You can pass a function name, a closure, or a file path to a template. If you skip render entirely, the block just outputs its InnerBlocks content.
It supports 35 field types (text, textarea, richtext, media, gallery, file, video, url, color, date, time, select, multi-select, autocomplete, taxonomy, repeater, group, tabpanels, innerblocks, post-selector, Google Maps, OpenStreetMap, and a few more). Full list is on the GitHub README.
GPL-2.0, pending review on wp.org, source here: https://github.com/salvatorecorsi/define-blocks
If you work with Gutenberg blocks regularly I'd like to know what you think. What's missing, what feels off, what would you do differently.


Top comments (0)