wordpress plugin · gpl

Define Blocks Gutenberg blocks in pure PHP

You describe the fields of a block in a PHP array. Define Blocks registers the block and builds its controls in the editor. The output is rendered in PHP, on the server.

not needed
  • react
  • jsx
  • block.json
  • npm install
  • webpack

There is no tagged release yet, so the download is the master branch. The zip extracts to define-blocks-master: rename the folder to define-blocks.

  • WordPress 6.6+
  • PHP 8.1+
  • 41 field types
  • GPL-2.0-or-later
  • v1.1.0

01 example

A custom Gutenberg block in 27 lines of PHP

The testimonial block below has four fields inside the block, a color setting in the sidebar and a toggle in the toolbar. This call is the whole definition. The markup is in blocks/testimonial.php, shown in 08 render.

functions.php
define_block_type( 'acme/testimonial', [
    'title'  => 'Testimonial',
    'icon'   => 'format-quote',
    'schema' => [
        [
            'scope'  => 'content',
            'fields' => [
                'quote'  => [ 'type' => 'richtext', 'label' => 'Quote' ],
                'author' => [ 'type' => 'text', 'label' => 'Author' ],
                'role'   => [ 'type' => 'text', 'label' => 'Role' ],
                'avatar' => [ 'type' => 'media', 'label' => 'Avatar', 'focus' => '1:1' ],
            ],
        ],
        [
            'scope'  => 'inspector',
            'panel'  => 'Card',
            'fields' => [ 'accent' => [ 'type' => 'color', 'label' => 'Accent' ] ],
        ],
        [
            'scope'  => 'toolbar',
            'fields' => [
                'featured' => [ 'type' => 'checkbox', 'label' => 'Featured', 'icon' => 'star-filled' ],
            ],
        ],
    ],
    'render' => __DIR__ . '/blocks/testimonial.php',
] );

Call it on init, from a theme or a plugin.

Mock of the Gutenberg editor with the Testimonial block: Quote, Author, Role and Avatar fields inside the block, a Card panel with an Accent color in the sidebar, and a Featured star toggle in the block toolbar.

testimonial

quote
The new site is much easier to update.
author
Anna Rossi
role
Marketing manager
avatar Select Image

Testimonial

Card

accent
Hover or focus a line of code to highlight its control, and the other way round. The numbers match each line to its control.

02 why

Custom Gutenberg blocks without React or a build step

Many custom blocks are small forms: a quote, a name, an image and a couple of settings. With the official tooling each of them becomes a JavaScript project with a block.json, an edit component and a build.

Define Blocks keeps the definition in PHP. The editor interface comes precompiled inside the plugin, so your project has nothing to build. Block definitions are plain PHP arrays and go through version control and code review with the rest of the theme.

nope

Shortcode

[testimonial quote="…" author="…" role="…" avatar="123" featured="1"]

Attributes are typed by hand. There is no media picker and no preview.

hmm

create-block, dynamic variant

  • block.json
  • index.js
  • edit.js
  • render.php
  • editor.scss
  • style.scss
  • package.json
  • node_modules/
  • build/

Every change needs npm run build. It is the right choice when the block needs custom editor behavior.

ok

Define Blocks

  • functions.php the define_block_type() call
  • blocks/testimonial.php optional template

Reload the editor to see the changes.

03 wordpress 7.0

WordPress 7.0 PHP-only blocks and Define Blocks

Since WordPress 7.0, released on 20 May 2026, core can register a block from PHP alone, using register_block_type(), the autoRegister support and a render callback. For a block with a few simple settings this is enough.

okcore, WordPress 7.0
register_block_type( 'acme/notice', [
    'title'           => 'Notice',
    'attributes'      => [
        'message' => [ 'type' => 'string', 'label' => 'Message', 'default' => '' ],
        'tone'    => [ 'type' => 'string', 'label' => 'Tone', 'enum' => [ 'info', 'warning' ], 'default' => 'info' ],
    ],
    'supports'        => [ 'autoRegister' => true ],
    'render_callback' => fn( $attributes ) => sprintf(
        '<p class="notice is-%s">%s</p>',
        esc_attr( $attributes['tone'] ),
        esc_html( $attributes['message'] )
    ),
] );

Core generates a text input and a select in the sidebar for these two attributes.

The controls are limited by design, and the core dev note says the feature is not meant to replace JavaScript blocks. Define Blocks covers the cases core leaves out and runs on WordPress 6.6 and later, including 7.0.

core 7.0

  1. controls for string, number, integer, boolean and enum attributes
  2. sidebar only
  3. no InnerBlocks support
  4. no media, repeater or rich text field
  5. WordPress 7.0 and later

define blocks

  1. 41 field types, including repeater, media, gallery and maps
  2. canvas, sidebar panels, Advanced panel, toolbar and toolbar dropdown
  3. one InnerBlocks area per block, with allowed blocks and templates
  4. conditions, validation, preview in the editor, post meta
  5. WordPress 6.6 and later

04 how it works

From a PHP array to the block editor

  1. 01

    describe the fields

    Each field in the array has a type and a label, and can have a default, a position in the editor, a condition and validation rules. Call define_block_type() on init: registrations are collected at priority 99.

  2. 02

    the editor controls are generated

    Define Blocks registers a native block (apiVersion 3) and places the controls where Gutenberg normally shows them. A post with an invalid visible field can't be saved until the field is fixed.

    PHP schema for a testimonial block. Hand-drawn braces and arrows link the quote, author, role and avatar fields to the matching controls in the Gutenberg block.
    Fig. 1: the fields in the schema and their controls in the block. Correction: 'mode' => 'crop' and 'aspectRatio' are not media options. 'focus' => '1:1' adds a focal point and a square preview.
  3. 03

    render on the server

    A closure, a template file or a function named after the block receives the values and returns HTML. The output is wrapped with get_block_wrapper_attributes(), so alignment, colors and custom classes work as in core blocks.

    A PHP render callback with a hand-drawn arrow pointing to the rendered testimonial card with quote, round avatar, name and role.
    Fig. 2: the render callback and the block on the front end. Correction: $block is the WP_Block instance, not the block attributes.

05 scopes

Fields on the canvas, in the sidebar and in the block toolbar

Every schema entry has a scope. The five scopes match the places where core blocks keep their controls. A scope can be repeated, and each inspector entry becomes a separate panel.

All fields write to the same values object, so a toggle in the toolbar can control a condition in the sidebar. Without scopes, an associative array of fields goes to the canvas.

On the canvas, inside the block.

content
[
    'scope'  => 'content',
    'fields' => [
        'name'  => [ 'type' => 'text', 'label' => 'Product name' ],
        'photo' => [ 'type' => 'media', 'label' => 'Photo' ],
    ],
],
inspector
[
    'scope'       => 'inspector',
    'panel'       => 'Pricing',
    'icon'        => 'money-alt',
    'initialOpen' => true,
    'fields'      => [
        'price'   => [ 'type' => 'number', 'label' => 'Price', 'min' => 0, 'step' => 0.01 ],
        'on_sale' => [ 'type' => 'checkbox', 'label' => 'On sale' ],
    ],
],
inspector-advanced
[
    'scope'  => 'inspector-advanced',
    'fields' => [
        'sku' => [ 'type' => 'text', 'label' => 'SKU' ],
    ],
],
toolbar
[
    'scope'  => 'toolbar',
    'fields' => [
        'highlight' => [ 'type' => 'checkbox', 'label' => 'Highlight', 'icon' => 'star-filled' ],
        'size'      => [
            'type'    => 'select',
            'label'   => 'Size',
            'icon'    => 'editor-expand',
            'options' => [ 'compact', 'full' ],
        ],
    ],
],
toolbar-dropdown
[
    'scope'  => 'toolbar-dropdown',
    'icon'   => 'cart',
    'label'  => 'Buy button',
    'fields' => [
        'buy_url' => [ 'type' => 'url', 'label' => 'Checkout URL' ],
        'new_tab' => [ 'type' => 'checkbox', 'label' => 'Open in a new tab' ],
    ],
],

Choose a scope to see where its fields appear.

06 field types

41 field types, including repeater, media, post selector and maps

Each type has its own editor control, a predictable value shape and a sanitizer. There are also 58 aliases, so image works as media and toggle as checkbox. Type names are case-insensitive, and you can register your own types.

Google maps and geocoders need an API key through defb_google_maps_key, Mapbox a token through defb_mapbox_key. OpenStreetMap needs neither. In the toolbar scope, select, checkbox, text, number and autocomplete switch to their toolbar versions automatically.

41 types

From a single line to rich text.

  • textSingle-line input. As number, a numeric input that stores a number.aka target, number
  • text-with-parserText input whose value runs through the defb.parse.{parser} JS filter.aka textwithparser, text_with_parser
  • textareaMultiline plain text, with configurable rows.aka text-area, code, json
  • richtextGutenberg RichText with inline formatting. Stores HTML.aka rich_text, rich-text, editor, classic-editor

Single and multiple choice, slider.

  • selectDropdown, one value. Options as strings, label/value pairs or posts.aka radio
  • multi-selectSearchable picker for several values.aka multiselect, multi_select, select-multi, multi-selects
  • autocompleteSearchable single choice.aka combobox
  • multi-autocompleteSearchable, several values.aka multiautocomplete, multi_autocomplete, multi-auto-complete
  • chipsFree tags, created while typing.aka tokenfield
  • checkboxToggle switch. Stores a boolean.aka toggle, switch
  • rangeSlider with min, max and step.

From the Media Library. Video is a URL.

  • mediaMedia Library picker. Optional focal point and preview ratio with focus.aka image, img, picture
  • fileAny file from the Media Library.
  • gallerySeveral images, sortable.
  • videoYouTube or Vimeo URL with an embed preview.

Links, dates, colors, posts and terms.

  • colorTheme palette plus custom colors. Optional alpha.
  • urlURL input, with post suggestions when postType is set.
  • linkURL, link text and a new-tab switch.aka cta, button
  • dateDate picker.aka datepicker
  • timeTime picker.
  • datetimeDate and time picker.aka datetimepicker
  • post-selectorSearch and pick posts, sortable, with thumbnails.aka postselector, post_selector, postselectorfield
  • taxonomyTerm search with parent paths. Can create terms.

Repeaters, groups, tabs and InnerBlocks.

  • repeaterRows of sub-fields with drag and drop, min/max and inline header fields.
  • groupVisual grouping. Values stay flat.
  • tabpanelsFields in tabs. Values stay flat.aka tabpanel, tab-panel, tab-panels, tabs
  • innerblocksNative InnerBlocks area, one per block.aka innerblock, inner-block, inner-blocks

Google Maps, Mapbox and OpenStreetMap, with a map or address only.

  • google-mapGoogle Maps with a draggable pin and reverse geocoding. Needs a key.aka map, maps, maptarget, map_target, map-target, map-bounds, map-pointer
  • osm-mapOpenStreetMap through Leaflet. No key.aka osm, openstreetmap, open-street-map, leaflet
  • mapbox-mapMapbox tiles through Leaflet. Needs a token.aka mapbox
  • google-geocoderGoogle address search, no map.aka geocoder, google-geocode
  • mapbox-geocoderMapbox address search, no map.aka mapbox-geocode
  • osm-geocoderNominatim address search, no map, no key.aka osm-geocode, openstreetmap-geocoder, open-street-map-geocoder

Controls for the block toolbar.

  • toolbar-toggleToolbar button that toggles on and off.
  • toolbar-selectToolbar dropdown menu, one value.
  • toolbar-dropdownToolbar menu that ticks the chosen option. Takes label/value options.
  • toolbar-inputToolbar button that opens a text or number input.
  • toolbar-autocompleteToolbar button that opens a searchable choice.

Hidden values, read-only values and headings.

  • hiddenNo UI. Stores a value.
  • valueRead-only, disabled input.
  • titleA heading between fields. Stores nothing.

07 conditions

Conditional fields in Gutenberg blocks

A field with show appears when its condition is true; hide does the opposite. Simple checks can be written as an array, the rest as a string expression. The same rules run in the editor (JavaScript) and on save (PHP), and hidden fields are not validated.

A malformed expression leaves the field visible and, with WP_DEBUG on, reports the error. in and contains compare plain values, not the {value, label} items of multi-select, chips or taxonomy fields.

compare
== != === !== > < >= <=. == is strict.
combine
&& || ! and parentheses.
lists
in [ … ], contains, .includes()
empty
items empty, items !empty. The operator always follows the field.
paths
settings.columns
repeater rows
$index (from 0) and root.field
array form
[ 'layout' => 'grid' ], a list of allowed values, [ 'operator' => '>=', 'value' => 3 ], and 'relation' => 'OR', 'XOR', 'NAND' or 'NOR'.

Try it

A Layout panel with working controls. Change them and the dependent fields appear or disappear.

layout panel
[
    'scope'  => 'inspector',
    'panel'  => 'Layout',
    'fields' => [
        'layout'       => [ 'type' => 'select', 'label' => 'Layout', 'default' => 'grid',
                            'options' => [ 'stack', 'split', 'grid' ] ],
        'columns'      => [ 'type' => 'range', 'label' => 'Columns', 'default' => 3,
                            'min' => 2, 'max' => 6,
                            'show' => 'layout == "grid"' ],
        'compact'      => [ 'type' => 'checkbox', 'label' => 'Compact cards',
                            'show' => 'layout == "grid" && columns >= 4' ],
        'media_side'   => [ 'type' => 'select', 'label' => 'Image side', 'default' => 'left',
                            'options' => [ 'left', 'right' ],
                            'show' => [ 'layout' => 'split' ] ],
        'equal_height' => [ 'type' => 'checkbox', 'label' => 'Equal height',
                            'hide' => 'layout == "stack"' ],
        'has_cta'      => [ 'type' => 'checkbox', 'label' => 'Add a button', 'default' => false ],
        'cta'          => [ 'type' => 'link', 'label' => 'Button',
                            'show' => 'has_cta' ],
        'cta_note'     => [ 'type' => 'text', 'label' => 'Small print',
                            'show' => 'has_cta && layout in ["stack", "split"]' ],
    ],
],

Layout

3

The demo reproduces the plugin's rules in a few lines of JavaScript.

Inside a repeater

Repeater rows have $index, starting from 0, and root, which points to the block's values. Use single quotes around the expression, otherwise PHP interpolates $index.

schema
'style'  => [ 'type' => 'select', 'label' => 'Style', 'options' => [ 'minimal', 'cards' ] ],
'slides' => [
    'type'       => 'repeater',
    'label'      => 'Slides',
    'min'        => 1,
    'max'        => 8,
    'previewKey' => 'title',
    'fields'     => [
        'title'   => [ 'type' => 'text', 'label' => 'Title', 'inline' => true ],
        'image'   => [ 'type' => 'media', 'label' => 'Image' ],
        'divider' => [
            'type'  => 'checkbox',
            'label' => 'Divider above',
            'show'  => '$index > 0 && root.style != "minimal"',
        ],
    ],
],

08 render

Server-side rendering and preview in the editor

render accepts a closure, a function name or the path to a template file. Without it, acme/hero looks for a function called acme_hero_render(). The callback is resolved at render time, so a plugin can register the block and a theme can provide the markup.

functions.php
define_block_type( 'acme/hero', [
    'title'  => 'Hero',
    'icon'   => 'cover-image',
    'schema' => [
        'heading' => [ 'type' => 'richtext', 'label' => 'Heading' ],
        'image'   => [ 'type' => 'media', 'label' => 'Background', 'focus' => true ],
    ],
    'render' => function ( array $attributes ): string {
        $values  = $attributes['values'] ?? [];
        $image   = $values['image'] ?? [];
        $focus_x = (float) ( $image['focus']['x'] ?? 0.5 ) * 100;
        $focus_y = (float) ( $image['focus']['y'] ?? 0.5 ) * 100;
 
        return wp_get_attachment_image( $image['id'] ?? 0, 'full', false, [
            'class' => 'hero__image',
            'style' => sprintf( 'object-position: %.1f%% %.1f%%', $focus_x, $focus_y ),
        ] ) . '<h2 class="hero__heading">' . wp_kses_post( $values['heading'] ?? '' ) . '</h2>';
    },
] );
blocks/testimonial.php
<?php
$values  = $attributes['values'] ?? [];
$classes = empty( $values['featured'] ) ? 'testimonial' : 'testimonial is-featured';
$accent  = ( $values['accent'] ?? '' ) ?: 'currentColor';
?>
<figure class="<?php echo esc_attr( $classes ); ?>" style="--accent: <?php echo esc_attr( $accent ); ?>">
    <blockquote><?php echo wp_kses_post( $values['quote'] ?? '' ); ?></blockquote>
    <figcaption>
        <?php echo wp_get_attachment_image( $values['avatar']['id'] ?? 0, 'thumbnail' ); ?>
        <strong><?php echo esc_html( $values['author'] ?? '' ); ?></strong>
        <span><?php echo esc_html( $values['role'] ?? '' ); ?></span>
    </figcaption>
</figure>

What the callback receives

$attributes['values'] is a flat array: groups and tabs don't add nesting. Fields without a default may be missing, so read them with ??. Rich text is HTML: print it with wp_kses_post().

Preview in the editor

With 'frontend_preview' => true the block toolbar shows a Preview button. The render runs through a REST request, which checks edit_post, and its HTML replaces the fields. The previous result stays visible while the next one loads. The shorthand also opens new blocks in preview mode; to have only the button, use 'settings' => [ 'frontendPreview' => true ].

Preview styles are not loaded automatically. Pass file paths or raw CSS to the defb_preview_style filter: the rules are scoped to .defb-preview and relative url()s are rebased.

functions.php
define_block_type( 'acme/quote', [
    'title'    => 'Quote',
    'settings' => [ 'frontendPreview' => true ],
    'schema'   => [ 'text' => [ 'type' => 'richtext', 'label' => 'Text' ] ],
    'render'   => __DIR__ . '/blocks/quote.php',
] );
 
add_filter( 'defb_preview_style', function ( $sources ) {
    $sources[] = get_theme_file_path( 'assets/css/blocks.css' );
    $sources[] = '.quote { border-left: 3px solid currentColor; padding-left: 1rem; }';
    return $sources;
} );
field typevalue
text, textarea, select, autocomplete, date, time, datetimestring
richtextHTML string
number (alias), rangeint or float
checkbox, toolbar-togglebool
url, videoURL string
colorhex, rgba() or another CSS color
mediaid, url, alt, title, sizes, and focus {x, y} when the field sets focus
gallerylist of id, url, alt, title
fileid, url, name, size, mime
linkurl, title, opensInNewTab
post-selectorlist of id, title, permalink, thumbnail
multi-select, multi-autocomplete, chips, taxonomylist of value, label
maps and geocoderslat, lng, latitude, longitude, formatted_address, address, query
repeaterlist of rows, each with a stable _defb_id

09 data

Reading block data with defb_get_block() and post meta

Values are stored in post_content, in the block's values attribute. defb_get_block() returns the first instance of a block, or a single field of it. defb_get_blocks() returns every instance, including those inside InnerBlocks, with defaults applied.

single.php
$post_id = get_queried_object_id();
 
$hero    = defb_get_block( $post_id, 'acme/hero' );
$heading = defb_get_block( $post_id, 'acme/hero', 'heading' );
$image   = $hero['image']['url'] ?? '';
 
foreach ( defb_get_blocks( $post_id, 'acme/slide' ) as $slide ) {
    printf( '<li>%s</li>', esc_html( $slide['title'] ?? '' ) );
}

Copying fields to post meta

With saveInMeta the value is copied to post meta on save, so WP_Query can filter by it. It is written only if the value validates and the user can edit that meta key. For keys starting with an underscore, register the meta with an auth_callback.

validate accepts rules (required, pattern, minLength, maxLength, min, max) or a callable. saveCallback receives ($value, $post_id, $field_key) for custom handling.

functions.php
define_block_type( 'acme/event', [
    'title'  => 'Event',
    'schema' => [
        'status' => [
            'type'       => 'select',
            'label'      => 'Status',
            'options'    => [ 'on-sale', 'sold-out', 'cancelled' ],
            'default'    => 'on-sale',
            'saveInMeta' => 'event_status',
        ],
    ],
    'render' => __DIR__ . '/blocks/event.php',
] );
 
$sold_out = get_posts( [
    'post_type'  => 'post',
    'meta_key'   => 'event_status',
    'meta_value' => 'sold-out',
] );

saveInMeta works on top-level fields and inside groups and tabs, not in repeater rows. With two instances of the same block in a post, the last one is saved. Removing the block does not delete the meta.

10 extend

Custom field types, hooks and schema diagnostics

A field type is a React component registered on window.DefineBlocks.Extensions and loaded with defb_register_custom_field( __FILE__ ). JSX is optional: build/index.js can be written by hand with createElement. A PHP sanitizer keeps the values clean on save and in previews.

acme-rating.php
add_action( 'plugins_loaded', function () {
    if ( ! function_exists( 'defb_register_custom_field' ) ) {
        return;
    }
 
    defb_register_custom_field( __FILE__, [ 'star-rating' ] );
    defb_register_custom_field_sanitizer( 'star-rating', fn( $value ) => max( 0, min( 5, (int) $value ) ) );
} );
build/index.asset.php
<?php return [ 'dependencies' => [ 'wp-element', 'wp-components' ], 'version' => '1.0.0' ];
build/index.js
const { createElement } = wp.element;
const { Button } = wp.components;
 
const StarRating = ( { id, field, value, onChange } ) => {
    const rating = Number( value ) || 0;
    const stars = [ 1, 2, 3, 4, 5 ].map( ( star ) =>
        createElement( Button, {
            key: star,
            icon: star <= rating ? 'star-filled' : 'star-empty',
            label: `Rate ${ star } of 5`,
            isPressed: star === rating,
            onClick: () => onChange( star === rating ? 0 : star ),
        } )
    );
 
    return createElement(
        'fieldset',
        { id, className: 'star-rating' },
        createElement( 'legend', null, field.label ),
        stars
    );
};
 
window.DefineBlocks = window.DefineBlocks || {};
window.DefineBlocks.Extensions = window.DefineBlocks.Extensions || {};
window.DefineBlocks.Extensions[ 'star-rating' ] = StarRating;
schema
'rating' => [ 'type' => 'star-rating', 'label' => 'Rating' ],

PHP filters: defb_register (definitions, per post), defb_preview_style, defb_google_maps_key, defb_mapbox_key, defb_editor_debug, defb_schema_field_types, defb_schema_field_properties.

JavaScript hooks, through @wordpress/hooks: defb.blockSettings, defb.ready.{block}, defb.change.{block}.{field}, defb.toggle.{block}.{field}, defb.toolbar.toggle, defb.parse.{parser}, defb.fieldRenderError.

With WP_DEBUG on, an unknown type or property, a duplicate name, a malformed condition, a reference to a missing field or a second InnerBlocks area triggers _doing_it_wrong() with the block name and the field path. defb_schema_diagnostics() returns the same list without notices, which is useful in tests.

parent, ancestor, usesContext and providesContext are passed to the native registration. providesContext maps to the whole values object, so read the key you need from it. Every block also receives postId and postType, which makes it work inside a Query Loop.

functions.php
define_block_type( 'acme/faq', [
    'title'           => 'FAQ',
    'providesContext' => [ 'acme/faqValues' => 'values' ],
    'schema'          => [
        'style' => [ 'type' => 'select', 'label' => 'Style', 'options' => [ 'plain', 'boxed' ] ],
        'items' => [ 'type' => 'innerblocks', 'allowedBlocks' => [ 'acme/faq-item' ] ],
    ],
] );
 
define_block_type( 'acme/faq-item', [
    'title'       => 'FAQ item',
    'parent'      => [ 'acme/faq' ],
    'usesContext' => [ 'acme/faqValues' ],
    'schema'      => [
        'question' => [ 'type' => 'text', 'label' => 'Question' ],
        'answer'   => [ 'type' => 'richtext', 'label' => 'Answer' ],
    ],
    'render'      => function ( array $attributes, string $content, WP_Block $block ): string {
        $values = $attributes['values'] ?? [];
        $style  = $block->context['acme/faqValues']['style'] ?? 'plain';
 
        return sprintf(
            '<details class="faq-item is-%s"><summary>%s</summary>%s</details>',
            esc_attr( $style ),
            esc_html( $values['question'] ?? '' ),
            wp_kses_post( $values['answer'] ?? '' )
        );
    },
] );

The plugin adds no script or stylesheet to the front end. The editor bundle loads only in the block editor when blocks are registered, and heavier fields such as maps and repeaters load on demand. The interface is translatable, ships with Italian and supports RTL.

11 compare

Define Blocks compared with ACF Blocks, WordPress 7.0 PHP-only blocks and other builders

Apart from the official React approach, none of these tools requires you to write JavaScript. They differ in where the block is defined, in price and in where the controls appear. The data comes from each project's documentation and plugin page, checked on 14 October 2026.

block builders for WordPress, 14 October 2026
toolpriceblock defined inJavaScript or buildwhere the controls arestatus
Define BlocksFree, GPL-2.0-or-laterOne define_block_type() call in PHP with fields, placement and renderNone. React only for custom field typesCanvas, sidebar panels, Advanced panel, block toolbar, toolbar dropdown. Optional server-rendered preview1.1.0. WordPress 6.6+, PHP 8.1+. On GitHub, no tagged release yet
WordPress 7.0 PHP-only blocks (autoRegister)Free, in coreregister_block_type() in PHP, with the autoRegister support and a render callbackNoneSidebar only, for string, number, integer, boolean and enum attributes. No InnerBlocks, no mediaWordPress 7.0+, released 20 May 2026
Core block.json + ReactFree, in coreblock.json and a JavaScript edit componentReact and JSX, with an npm buildAnywhere, as codedThe official approach
ACF PRO Blocks / Secure Custom FieldsACF PRO from $49 to $249 per year. SCF is freeblock.json via register_block_type(), plus a field group (admin UI, PHP or Local JSON) with a Block location ruleNoneSidebar with live preview (Blocks V3, ACF 6.6). Optional inline editing since ACF 6.7ACF maintained by WP Engine. SCF 6.9.5, 100k+ installs, maintained by WordPress.org
Lazy BlocksFree. Pro from $49 to $299 per yearVisual builder in wp-admin, or PHP through lzb/init. Export and importNone21 free controls, including repeater and InnerBlocks. Conditions, panels and post, taxonomy and user controls are in Pro4.4.2 (12 Oct 2026), 20k+ installs, WordPress 6.2+, PHP 8.0+
BlockstudioFree, GPL. Optional paid Plus, one-timeA folder per block: block.json with a blockstudio key, plus a PHP, Twig or Blade templateNo build step: SCSS, ES modules and server-side Tailwind27 field types, including repeater. RichText and InnerBlocks in templates7.6.14. WordPress 6.7+, PHP 8.2+
MB Blocks (Meta Box)$59 per year for unlimited sites, or in Meta Box bundles. Not in the free Meta Box LiteA PHP array in Meta Box syntax, or the MB Builder UINo React, webpack or BabelLive previewActive
Carbon FieldsFree, GPL-2.0, installed with ComposerPHP fluent API: Block::make()->add_fields()->set_render_callback()NoneEdit and preview modes through set_mode()3.6.11 (27 Aug 2026). Block documentation last updated in 2021

When another tool is a better fit

The block has a few scalar settings and the site runs WordPress 7.0
core autoRegister
The block needs custom editor interactions or client-side rendering
React
You also need fields outside blocks, on options pages or in user and term meta
ACF or Secure Custom Fields
The people building blocks don't write code
Lazy Blocks
You want Twig or Blade templates and a folder per block
Blockstudio
You need installation from wordpress.org or Composer with automatic updates
not Define Blocks, for now

12 install

Installing Define Blocks (WordPress 6.6+, PHP 8.1+)

  1. Download the master branch zip from GitHub. There is no tagged release yet.
  2. Extract it into wp-content/plugins and rename define-blocks-master to define-blocks.
  3. Activate the plugin and call define_block_type() on init. Registrations are collected at priority 99; later calls are ignored and reported when WP_DEBUG is on.
terminal
cd wp-content/plugins
curl -L -o define-blocks.zip https://github.com/corsivo-suite/define-blocks/archive/refs/heads/master.zip
unzip define-blocks.zip
mv define-blocks-master define-blocks
rm define-blocks.zip
wp plugin activate define-blocks
terminal
git clone https://github.com/corsivo-suite/define-blocks.git wp-content/plugins/define-blocks
wp plugin activate define-blocks
functions.php
add_action( 'init', function () {
    if ( ! function_exists( 'define_block_type' ) ) {
        return;
    }
    define_block_type( 'acme/hello', [
        'title'  => 'Hello',
        'schema' => [ 'name' => [ 'type' => 'text', 'label' => 'Name', 'default' => 'world' ] ],
        'render' => function ( array $attributes ): string {
            return '<p>Hello, ' . esc_html( $attributes['values']['name'] ?? 'world' ) . '.</p>';
        },
    ] );
} );

The plugin is not on wordpress.org or Packagist. To update it, replace the folder or run git pull. Wrapping your calls in function_exists() keeps the site working if the plugin is deactivated. The wp commands require WP-CLI; the Plugins screen works as well.

define blocks

v1.1.0 · defineblocks.dev
order: 1 block

react0
jsx0
block.json0
npm install0
webpack0
build step0
node_modules0 mb
files touched1
field types41
aliases58
licensegpl
total0.00

paid with: one php call

thank you

13 faq

Gutenberg blocks in PHP: frequently asked questions

Can I build a custom Gutenberg block without React or a build step?

Yes. Call define_block_type() with a schema and a render callback. The editor interface is precompiled in the plugin, so there is nothing to build in your project. The result is a standard block, registered with register_block_type() and rendered on the server.

WordPress 7.0 has PHP-only blocks. Why use a plugin?

If the block only needs a few text, number, boolean or enum settings in the sidebar, core autoRegister is enough. Core doesn't support InnerBlocks, media fields or controls on the canvas and in the toolbar. Define Blocks supports them, adds repeaters and maps, and works on WordPress 6.6 and later.

Is Define Blocks an alternative to ACF Blocks?

Yes, with a different workflow. Secure Custom Fields already offers ACF-style blocks for free, so price is not the difference. ACF and SCF combine a block.json with a field group and a location rule. With Define Blocks a single PHP call contains fields, placement and render, and fields can also go on the canvas or in the toolbar. If you need fields outside blocks, such as options pages or term meta, ACF or SCF is the better choice.

How do I add a repeater field to a Gutenberg block?

Use 'type' => 'repeater' with its own fields. Rows can be reordered by drag and drop or with the keyboard. min and max limit the number of rows, previewKey sets the row label and inline moves a sub-field into the row header. The render callback receives a list of rows.

How do I show or hide block fields conditionally?

Add show or hide to the field, as an array like [ 'layout' => 'grid' ] or as a string like 'layout == "grid" && columns >= 4'. Conditions are evaluated in the editor and on save, and hidden fields are not validated.

How do I preview a dynamic block inside the editor?

Add 'frontend_preview' => true. A Preview button appears in the block toolbar; it renders the callback through the REST API and shows the HTML in place. Styles are passed through the defb_preview_style filter and scoped to the preview.

Where is block data stored, and can I query it?

In post_content, in the block's values attribute. Read it with defb_get_block() or defb_get_blocks(). To filter posts with WP_Query, add saveInMeta to the field and the value is copied to post meta on save.

What happens to my blocks if I deactivate the plugin?

No data is deleted: values stay in post_content and mirrored meta stays in the database. The block types are no longer registered, so the blocks render nothing on the front end (a block with InnerBlocks still prints its children) and the editor marks them as unsupported. Reactivating the plugin restores them. Calling define_block_type() while the plugin is inactive causes a fatal error, so wrap the calls in function_exists( 'define_block_type' ).

Does it work with classic themes and block themes?

Yes. The blocks are native and rendered on the server, so the type of theme doesn't matter. Inside a Query Loop they receive postId and postType through block context.

Can I write my own field types?

Yes. A field type is a React component registered on window.DefineBlocks.Extensions and loaded with defb_register_custom_field( __FILE__ ). JSX is optional: createElement in a hand-written file works without a build.

Is it on wordpress.org or Packagist?

Not yet, and there is no tagged release. Download the master branch zip from GitHub or clone the repository into wp-content/plugins/define-blocks. To update, replace the folder or run git pull.

Does it load anything on the front end?

No. The plugin adds no script or stylesheet to the front end, only your HTML inside the block wrapper. The editor bundle loads only in the block editor when blocks are registered, and heavier fields load on demand.