Skip to main content
Signocore
WordPress PHP Web Development Updated 7 min read Daniel Nielsen

WordPress Blocks Without a Build Step: What PHP-Only Blocks Can and Can't Do

WordPress 7.0 lets you register a block in PHP alone and builds its settings for you. How it works, where it stops, and when you still need React.

WordPress Blocks Without a Build Step: What PHP-Only Blocks Can and Can't Do

For years, building a custom WordPress block meant the same ritual: scaffold with create-block, install a few hundred megabytes of node_modules, write an edit component in React, run wp-scripts in watch mode, and remember to rebuild before every release. For a block that renders a notice with a heading and a color, that is a lot of machinery.

WordPress 7.0, released in May 2026, quietly changed that. You can now register a block in PHP alone and let the editor build its settings panel for you. No JavaScript, no build step, no package.json. I like this feature a lot, but it has sharp edges that the announcement posts gloss over, so this guide covers both: how PHP-only blocks work, and where they stop.

If you want the classic toolchain first, the guide to building custom Gutenberg blocks walks through block.json, React edit components and wp-scripts. This article is about the shortcut.

What Changed in WordPress 7.0

The change is one flag. Register a block with register_block_type(), give it a render_callback, and add autoRegister to its supports. Both are required: core only exposes the block to the editor when the flag is set and a render callback exists.

Behind the scenes, core prints a small inline list of these blocks into the editor. The editor registers each one with a generic edit component that renders a server-side preview, and with a save function that returns nothing. In other words, every PHP-only block is a dynamic block: the HTML is produced by your PHP on every page view, never stored in the post content.

The key is camelCase, autoRegister. Early versions of the feature in the Gutenberg plugin used a snake_case key, so if a tutorial shows auto_register, it predates WordPress 7.0 and will not work.

A PHP-Only Block, Start to Finish

Here is a complete notice block with a message, a tone and a toggle for an icon. It also opts into color and padding controls, which work through the normal block supports system.

add_action( 'init', function () {
    register_block_type( 'acme/notice', array(
        'title'           => __( 'Notice', 'acme' ),
        'category'        => 'text',
        'attributes'      => array(
            'message'  => array(
                'label'   => __( 'Message', 'acme' ),
                'type'    => 'string',
                'default' => '',
            ),
            'tone'     => array(
                'label'   => __( 'Tone', 'acme' ),
                'type'    => 'string',
                'enum'    => array( 'info', 'warning', 'success' ),
                'default' => 'info',
            ),
            'showIcon' => array(
                'label'   => __( 'Show icon', 'acme' ),
                'type'    => 'boolean',
                'default' => true,
            ),
        ),
        'supports'        => array(
            'autoRegister' => true,
            'color'        => array( 'background' => true, 'text' => true ),
            'spacing'      => array( 'padding' => true ),
        ),
        'render_callback' => function ( $attributes ) {
            $wrapper = get_block_wrapper_attributes( array(
                'class' => 'acme-notice is-tone-' . sanitize_html_class( $attributes['tone'] ),
                'role'  => 'note',
            ) );

            return sprintf(
                '<div %1$s>%2$s<p>%3$s</p></div>',
                $wrapper,
                $attributes['showIcon'] ? '<span class="acme-notice__icon" aria-hidden="true">i</span>' : '',
                esc_html( $attributes['message'] )
            );
        },
    ) );
} );

Three details matter more than they look:

  • The wrapper attributes. get_block_wrapper_attributes() is what turns the color and padding a user picks into classes and inline styles on the front end. Leave it out and the controls appear in the editor but do nothing on the page.

  • One root element. The editor merges its own block props onto the top-level element of your output. Return a single wrapper and everything lines up.

  • Escaping. The render callback is a normal PHP function that outputs HTML, so the usual rules apply. Escape every attribute you print.

What the Editor Builds for You

The settings panel is generated from your attributes. Each attribute of type string, number, integer or boolean becomes a field in a panel called Settings in the block inspector:

  • A string becomes a text field.

  • An integer or number becomes a number field.

  • A boolean becomes a checkbox.

  • A string with an enum becomes a select.

Attributes that read their value from the saved HTML (anything with a source) are skipped, and so are the attributes that block supports add on their own, such as style or backgroundColor. That also means you should never name one of your own attributes style.

The label key is optional, but use it. Without it, the field label is the raw attribute name, so showIcon appears as "showIcon". Labels registered in PHP can be translated with __(), which is one more reason to register these blocks in PHP rather than in block.json.

Select options show their raw values, because there is no way to map a value to a separate label. Pick enum values that read well on their own: warning works, wrn does not.

The Trade-Offs Nobody Puts in the Headline

Every convenience here has a price, and it is paid in the editor.

  • Every change is a server round trip. The preview comes from the REST endpoint /wp/v2/block-renderer/, called once when the block appears and again after each change, with a short debounce. During each request, the preview is replaced by a loading placeholder, so it flickers while someone types. Miguel Fonseca, who wrote the announcement, put it plainly in the comments under it: "any in-editor changes to PHP-only blocks will require server roundtrips".

  • The preview is not interactive. It is rendered inert, and scripts and inline event handlers are stripped from it. What you see in the editor is a picture of the front end, not the front end.

  • No nested blocks. InnerBlocks are not supported, and the core team has said there is no clear plan for it.

  • No typing inside the block. Rich text, media pickers and link fields in the canvas are not supported, and the core team has made no promises about adding them. Today, everything is edited in the sidebar.

  • Always dynamic. There is no save function and none is planned, so the markup is generated on every request. For most blocks that is fine. For heavy queries, cache inside your render callback.

One version note: on WordPress 7.0.x the server-side preview does not receive the current post ID, so a block that reads post data shows the wrong thing in the editor. A fix is being tracked in Gutenberg issue #78619 - check the status before relying on it.

Does It Work With block.json?

Through the code path, yes. A block.json with a "render" file sets the render callback, and the auto-register flag is read from supports like any other. But the official block.json schema only learned about autoRegister in recent Gutenberg releases, there is no official example of the combination, and labels in block.json are not translatable. My advice for now: register PHP-only blocks in PHP, and keep block.json for blocks that also ship JavaScript.

Where @wordpress/build Fits

You may have read about @wordpress/build, the new build tool the WordPress team introduced in April 2026. It replaces webpack and Babel with esbuild, works by convention instead of configuration, and generates the PHP that registers your scripts. It is a promising direction.

It is also not a way to build blocks yet. Support for a blocks folder is still an open issue, and the announcement itself tells everyone outside early adopters to "stay on @wordpress/scripts". So in late 2026, "no build step" for blocks means PHP-only blocks, not a faster bundler.

A Side Benefit: Immune to the Iframe Change

Since WordPress 7.1, the post editor always runs inside an iframe, whatever theme or block API version is in play. Editor code that reaches for the global document or window to touch the canvas now finds the wrong document, and older jQuery-style integrations break in subtle ways. The fix for classic blocks is to work through element.ownerDocument and its defaultView.

PHP-only blocks sidestep the problem entirely, because they have no edit code of their own. If you maintain a handful of small blocks that broke in 7.1, moving the simplest ones to PHP-only registration can be faster than patching their editor scripts.

When You Still Need JavaScript

PHP-only blocks are the wrong tool when the editor experience is the product:

  • Layout blocks that hold other blocks, such as columns, tabs or sections.

  • Blocks where people type directly in the canvas, like a quote or a call-out with formatted text.

  • Anything that needs live interaction in the editor, such as a map you drag or a chart you configure visually.

Front-end interactivity is a separate question. The Interactivity API, which gained a watch() helper in WordPress 7.0, adds behavior to server-rendered HTML through directives, so in principle it pairs well with a PHP render callback. The combination is not documented yet, though, so build a small prototype before you commit a client project to it.

My Rule of Thumb

If a block is "settings in, HTML out", I register it in PHP: notices, call-to-action boxes, cards, lists driven by a query, small layout helpers with a few options. If editors type inside it or nest other blocks in it, it gets the full toolchain from the classic block guide, ideally with types from TypeScript for PHP developers.

The test is honest and quick: write the render callback first. If the whole block fits in it, you were never going to need React.

// keep reading

Have questions about this article?

Get in touch if you'd like to learn more about this topic.