You want to build a custom WordPress block. But you don’t want to learn React, manage a build pipeline, or deal with NPM packages.

Seven and a half years after blocks arrived in Core, WordPress introduces a way to build blocks without any of these things. All you need is PHP.

But was the long wait worth it?

A radically simplified block building experience

A traditional WordPress block needs to be registered twice. Once in PHP, and once in JavaScript.

But WordPress 7.0 introduces a new and streamlined approach, allowing you to register a block using only PHP.

Registering a block using only PHP

Let’s use this feature to build a Hello World block:

function css_tricks_hello_world_block() {
  register_block_type(
    'css-tricks/hello-world',
      [
        'title' => 'Hello World',
        'render_callback' => function () {
          return sprintf(
            '<div %s>Hello World!</div>',
            get_block_wrapper_attributes()
          );
        },
        'supports' => [
          'autoRegister' => true,
        ],
      ]
  );
}
add_action('init', 'css_tricks_hello_world_block');

The block is fully functional in the block editor, and fits right in with all the other blocks:

Block inserted into the WordPress block editor, containing a heading that reads PHP Block Registration followed by a paragraph of text that says Hello World. There are no block settings in the sidebar.

The key addition is the 'autoRegister' => true flag in the supports section. When set, WordPress automatically generates the required JavaScript for your block based on the PHP registration. This includes the client-side registration and the editor preview.

Adding attributes to PHP-only registered blocks

Attributes let users customize the block’s appearance and behavior. In traditional block development you not only need to define the attributes, but also build out the corresponding controls in the editor interface.

With PHP-only registration, all that is needed is the attributes definition during block registration:

function css_tricks_hello_world_block()
{
  register_block_type(
    'css-tricks/hello-world',
    [
      'title' => 'Hello World',
      'render_callback' => function ($attributes) {
        return sprintf(
          '<div %s>%s</div>',
          get_block_wrapper_attributes(),
          esc_html($attributes['greeting'])
        );
      },
      'supports' => [
        'autoRegister' => true,
      ],
      'attributes' => [
        'greeting' => [
          'type' => 'string',
          'default' => 'Hello World!',
        ],
      ],
    ]
  );
}
add_action('init', 'css_tricks_hello_world_block');

This code registers a greeting attribute as a string with a default value. WordPress generates the corresponding input control in the block’s Settings sidebar.

A block inserted into the WordPress block editor displaying the sidebar Settings, which includes a text field for the greeting.

At first sight, there is a lot to like about PHP-only registered blocks. For any WordPress developer, it feels like the good old times when programming was simpler.

Limitations of PHP-only registered blocks

You might be tempted to delay learning JavaScript block development indefinitely. But the PHP-only approach has important limitations worth understanding.

No interactions with the content of the block

The editor displays the HTML as returned by the block’s render_callback PHP function. When the block is first displayed or when the user interacts with one of its controls, the editor component requests a new PHP render from a REST API endpoint.

While blocks rendered this way integrate seamlessly into the editor, they are not part of the single-page JavaScript application that powers the entire editor experience.

This creates two key limitations:

First, you cannot add any controls within the block preview. This means you are limited to the auto-generated controls in the Settings sidebar.

The default interaction mode with blocks is the block preview itself. Imagine that you need a testimonial block. With a JavaScript-rendered block, you would build out the testimonial design and allow editing in place.

With a PHP-rendered block you can only use the sidebar. And even here you are limited, as there is currently no support for image uploads or multiline text.

With JavaScript, you can allow editing in the block as well as in the sidebar, and you have access to all the controls that WordPress Core uses, with the ability to implement your own. Without JavaScript, you’ll always be limited to the options WordPress provides based on the registered attributes of your PHP-only block.

Secondly, you cannot attach any JavaScript to markup in the block preview. Imagine you want to develop a block that pulls five related posts and displays them in a slider. For that you would output the markup and then pass a DOM node to a JavaScript library that transforms the raw markup into the desired slider interface.

This reliance on finding and manipulating DOM elements is typical for traditional JavaScript development. But with PHP-only registered blocks in the block editor, the markup is fetched asynchronously and replaced on every re-render. This makes interacting with the DOM of the block preview unreliable or impossible.

While the front-end render works fine with JavaScript libraries, the editor authoring experience will not work correctly. Even if you manage to attach event listeners on first load, these will be disconnected the moment the preview re-renders.

These limitations are caused by the architecture of this feature and will not change in the future.

No access to fresh data

On the initial load of the block editor, WordPress loads the post data from the database into a client-side store managed by JavaScript. Any changes you make in the editor update this data store on the client side, but the database isn’t updated until you save the post.

PHP-only registered blocks bypass this client-side store. When a block renders, it queries the database directly. But the database might contain stale data compared to what’s currently in the editor. The PHP-rendered block isn’t notified of changes in the client-side data, so it can’t refresh when data changes.

As a practical example: imagine you are building a block that displays a header element with the post title. When the user changes the title in the editor, your block will still show the value from the database. The user would need to save the post and reload the editor for the changed title to appear in the PHP-only block.

This makes PHP-only blocks unsuitable for any block that displays data the user can change in the editor, such as the title, content, excerpt, featured images, or attached terms.

No access to the current post context

PHP-only registered blocks render through a REST API endpoint, so the same code renders both the editor preview and the front end. But there is a critical difference: global state.

On the front end, blocks render within The Loop, which sets key global variables like $post. Template tags like the_title() or the_content() rely on these globals to know which post is displayed.

REST APIs are stateless and don’t rely on global state. The endpoint that renders the block editor preview accepts a post ID parameter, but the editor component does not pass it through. This means your render callback has no way to know which post is being edited.

This limits the functions you can use in the block editor preview. Template tags and functions like get_post_meta() need post context to work correctly.

This is a significant architectural limitation as of WordPress 7.0. It could be addressed by passing the post ID to the endpoint, but there are no concrete plans to change this at the time of writing.

Limited attribute types and editing interfaces

WordPress 7.0 supports only three attribute types: strings, numbers, and booleans. These map to four basic editor controls: text inputs, number inputs, checkboxes, and a dropdown.

The screenshot below shows a block that uses all available user interface elements:

Block sidebar settings showing example controls for string, integer, boolean, and dropdown.

The dropdown is the only advanced control, but it has a significant limitation: it does not support keyed arrays. This makes it impossible to have a label that differs from the stored value, which restricts how you can present options to users in a meaningful way.

PHP-only block registration in WordPress 7.0 is a genuine step forward for developers who want to avoid JavaScript entirely, but the architectural constraints around editor interactivity, live data, post context, and attribute types mean it works best for simple, static blocks. For anything more complex, JavaScript block development remains the more capable path.