Block Registration Only via PHP

In WordPress, you can create a Gutenberg block without your own JavaScript registration, React, and building through npm. To do this, you need to register a dynamic block via register_block_type(), specify a PHP render function, and enable supports.autoRegister.

WordPress will add the block to the editor on its own, show a server-side preview, and create simple settings fields in the sidebar.

You need WordPress 7.0 or newer. In earlier core versions, a single render_callback is not enough for the block to appear in the editor.

How it works

  1. On the init action, PHP registers the block type.
  2. supports.autoRegister = true enables automatic registration in the editor. You also need an invoked render_callback.
  3. The editor receives HTML via the REST API and displays it as a preview. After changing settings, it requests updated HTML.
  4. On the site, WordPress calls the same PHP function when rendering the block.

You do not need to write your own edit() and save(). The editor’s JavaScript will keep working, but it is provided by WordPress.

Examples

#1 Minimal block

Let’s create a plugin wp-content/plugins/kama-php-blocks/kama-php-blocks.php:

<?php
/**
 * Plugin Name: Kama PHP Blocks
 * Requires at least: 7.0
 */

add_action( 'init', 'kama_register_notice_block' );

function kama_register_notice_block() {
	register_block_type( 'kama/notice', [
		'api_version'     => 3,
		'title'           => 'Notice',
		'category'        => 'widgets',
		'icon'            => 'info-outline',
		'render_callback' => 'kama_render_notice_block',
		'supports'        => [
			'autoRegister' => true,
			'html'         => false,
		],
	] );
}

function kama_render_notice_block( $attributes, $content, $block ) {
	return sprintf(
		'<div %s>Important information for visitors.</div>',
		get_block_wrapper_attributes()
	);
}

Activate the plugin and find “Notice” in the editor’s block list.

  • kama/notice - a unique name in the format namespace/block-name. Use lowercase Latin letters, numbers, and hyphens; start the name with a letter.
  • title, category, icon - the name, category, and icon. A Dashicon is specified without the dashicons- prefix.
  • api_version - the Block API version. For a PHP-only block, the editor uses a minimum of 3, even if an older version is specified.
  • html => false - disables the HTML editing option. This is a UI setting, not a mandatory condition for auto-registration.

Do not limit the registration by checking is_admin(): the block is needed both on the site and in REST API. A successful call to register_block_type() returns a WP_Block_Type object; on error, it returns false.

Render function

During normal rendering, WordPress passes three arguments to the callback:

function kama_render_notice_block( $attributes, $content, $block ) {
	return '<div>Block contents</div>';
}
  • $attributes (array)

    • attribute values including the schema and default values.
  • $content (string)

    • saved inner markup. For an automatically registered block without nested content, it is usually empty.
  • $block (WP_Block{})
    • the current block instance, including its context.

The callback must return an HTML string. For a large template, you can use ob_start() and return ob_get_clean().

get_block_wrapper_attributes() builds attributes for the root element: classes, styles, and other supported settings. The resulting string is inserted into the opening tag without calling esc_attr() again.

#2 Attributes and automatic settings fields

Let’s add a message text, a toggle, and a styling selector. In the first example, replace both functions with the following code, keeping the add_action() string:

function kama_register_notice_block() {
	register_block_type( 'kama/notice', [
		'api_version' => 3,
		'title'       => 'Notice',
		'category'    => 'widgets',
		'icon'        => 'info-outline',
		'attributes'  => [
			'message' => [
				'label'   => 'Message text',
				'type'    => 'string',
				'default' => 'Important information for visitors.',
			],
			'showTitle' => [
				'label'   => 'Show title',
				'type'    => 'boolean',
				'default' => true,
			],
			'tone' => [
				'label'   => 'Styling',
				'type'    => 'string',
				'enum'    => [ 'info', 'success', 'warning' ],
				'default' => 'info',
			],
		],
		'supports' => [
			'autoRegister' => true,
			'html'         => false,
		],
		'render_callback' => 'kama_render_notice_block',
	] );
}

function kama_render_notice_block( $attributes, $content, $block ) {

	$wrapper_attributes = get_block_wrapper_attributes( [
		'class' => 'kama-notice is-tone-' . sanitize_html_class( $attributes['tone'] ),
	] );

	$title = $attributes['showTitle'] ? '<h2>Please pay attention</h2>' : '';

	return sprintf(
		'<div %1$s>%2$s<p>%3$s</p></div>',
		$wrapper_attributes,
		$title,
		esc_html( $attributes['message'] )
	);
}

In the sidebar, fields for all three attributes will appear. The tone values will turn into selection options, and the CSS class is-tone-* will allow styling each option.

Which fields are created automatically

Attribute schema Field in the editor
type => 'string' A text field.
type => 'integer' A numeric field for whole values.
type => 'number' A numeric field that allows decimals.
type => 'boolean' A toggle switch.
type with enum A selection from predefined values.
  • label sets the label. Without it, the attribute name is used.
  • default sets the initial value. It must match type and, if present, enum.
  • For an auto-field, explicitly specify the supported type: providing only one enum is not enough.
  • The enum option labels are formed from the values themselves. Separate labels for them are not provided by this mechanism.

Examples of numeric attributes:

'attributes' => [
	'count' => [
		'label'   => 'Number of posts',
		'type'    => 'integer',
		'default' => 5,
	],
	'price' => [
		'label'   => 'Price',
		'type'    => 'number',
		'default' => 9.99,
	],
],

When a field is not created

Auto-generation skips the attribute if:

  • It has a non-empty source, for example html, text, or attribute.
  • It has role => 'local'. Such values are also not saved in the post content.
  • Its type is different from string, integer, number, boolean. For example, there are no auto-fields for array and object.

Declaring the attribute itself does not create a RichText, image picker, repeater, multiline field, or an arbitrary UI component. For that, you need your own JavaScript.

Before the PHP render, use esc_html() for text, esc_url() for URLs, esc_attr() for individual HTML attributes, and wp_kses_post() for allowed HTML markup.

#3 Colors, spacing, typography, and alignment

Standard styling capabilities are enabled separately from user attributes. Replace supports in the previous example:

'supports' => [
	'autoRegister'    => true,
	'html'            => false,
	'anchor'          => true,
	'align'           => [ 'wide', 'full' ],
	'customClassName' => true,
	'color' => [
		'text'       => true,
		'background' => true,
		'gradients'  => true,
	],
	'spacing' => [
		'margin'  => true,
		'padding' => true,
	],
	'typography' => [
		'fontSize'   => true,
		'lineHeight' => true,
	],
],

Leave get_block_wrapper_attributes() on the root element: it is used to apply the selected settings.

color in supports enables the standard color interface for the block. An arbitrary attribute type => 'string' with the name color by itself will not create a color palette.

#4 Register via block.json and a separate template

A PHP-only block can be described in block.json. This is the recommended WordPress way to store metadata; a JavaScript registration file is still not needed.

In the plugin folder, create a blocks/notice/ directory with block.json, render.php, and style.css files.

block.json:

{
	"$schema": "https://schemas.wp.org/trunk/block.json",
	"apiVersion": 3,
	"name": "kama/notice",
	"title": "Notice",
	"category": "widgets",
	"icon": "info-outline",
	"attributes": {
		"message": {
			"label": "Message text",
			"type": "string",
			"default": "Important information for visitors."
		}
	},
	"supports": {
		"autoRegister": true,
		"html": false,
		"color": {
			"text": true,
			"background": true
		}
	},
	"style": "file:./style.css",
	"render": "file:./render.php"
}

render.php is loaded for each block instance. Do not declare common functions and classes in it: if it is loaded again, you will get a “re-declaration” error.

In the main plugin file, replace the registration from the previous examples (this is an alternative option):

add_action( 'init', 'kama_register_notice_block' );
function kama_register_notice_block() {
	register_block_type( __DIR__ . '/blocks/notice' );
}

render.php:

<div <?= get_block_wrapper_attributes( [ 'class' => 'kama-notice' ] ) ?>>
	<p><?= esc_html( $attributes['message'] ) ?></p>
</div>

style.css:

.kama-notice {
	padding: 1rem;
	border-inline-start: 4px solid currentColor;

	> :first-child {
		margin-block-start: 0;
	}

	> :last-child {
		margin-block-end: 0;
	}
}

Styles and scripts

In block.json When registering with a PHP array Purpose
style style_handles CSS for the editor and site.
editorStyle editor_style_handles CSS for the editor only.
viewStyle view_style_handles CSS for the site only.
viewScript view_script_handles Regular JavaScript for the site.
viewScriptModule view_script_module_ids JavaScript module for the site.

In block.json, you can specify a path like file:./style.css.

In PHP, you pass an array of handles for pre-registered resources:

wp_register_style(
	'kama-notice',
	plugins_url( 'blocks/notice/style.css', __FILE__ ),
	[],
	'1.0.0'
);

register_block_type( 'kama/notice', [
	'title'           => 'Notice',
	'category'        => 'widgets',
	'style_handles'   => [ 'kama-notice' ],
	'render_callback' => 'kama_render_notice_block',
	'supports'        => [ 'autoRegister' => true ],
] );

#5 Dynamic data: recent posts

A block can get data through any suitable WordPress PHP functions. For a list of posts, we’ll register a separate type:

add_action( 'init', 'kama_register_recent_posts_block' );

function kama_register_recent_posts_block() {
	register_block_type( 'kama/recent-posts', [
		'title'    => 'Recent posts',
		'category' => 'widgets',
		'icon'     => 'list-view',
		'attributes' => [
			'count' => [
				'label'   => 'Number of posts',
				'type'    => 'integer',
				'default' => 5,
			],
		],
		'supports' => [
			'autoRegister' => true,
			'html'         => false,
		],
		'render_callback' => 'kama_render_recent_posts_block',
	] );
}

function kama_render_recent_posts_block( $attributes, $content, $block ) {
	$posts = get_posts( [
		'numberposts' => (int) $attributes['count'],
		'post_status' => 'publish',
	] );

	if ( ! $posts ) {
		return sprintf( '<div %s>No posts yet.</div>', get_block_wrapper_attributes() );
	}

	$html = '';

	foreach ( $posts as $item ) {
		$html .= sprintf(
			'<li><a href="%1$s">%2$s</a></li>',
			esc_url( get_permalink( $item ) ),
			esc_html( get_the_title( $item ) )
		);
	}

	return sprintf( '<ul %1$s>%2$s</ul>', get_block_wrapper_attributes(), $html );
}

Current post data and context

To get a post ID through a block instance, add to the registration:

'uses_context' => [ 'postId', 'postType' ],

In the callback, you can read the meta-field of this post:

$post_id = (int) ( $block->context['postId'] ?? 0 );
$value   = $post_id ? get_post_meta( $post_id, 'reading_time', true ) : '';

return sprintf(
	'<div %1$s>%2$s</div>',
	get_block_wrapper_attributes(),
	esc_html( $value )
);

In block.json, the corresponding property is called usesContext. Context is useful in templates and inside a Query Loop.

For the editor server-side preview, it passes postId. Arbitrary ancestor context is not automatically carried over into a separate REST request: if the callback depends on it, separately verify that the preview matches the output on the site.

What else can be configured

PHP-only registration keeps standard block metadata. To choose the needed parameter, take into account the difference in names:

PHP parameter In block.json Capability
description description Description and keywords for searching the block.
keywords keywords Description and keywords for searching the block.
example example Example of attributes for preview in the block list.
styles styles Named styling variants via CSS classes.
variations variations Ready-made block variants with different attributes.
parent parent Limits availability to the direct parent.
ancestor ancestor Limits availability to an ancestor at any depth.
block_hooks blockHooks Automatic insertion next to other blocks.

Properties parent and ancestor limit where the block can be inserted, but do not create an editing area inside it for child blocks.

For the full list of arguments, see register_block_type().

How the block is stored and rendered

Inside the post content, a comment is saved with the block name and its settings, for example:

<!-- wp:kama/notice {"message":"Site updated","tone":"success"} /-->

Values matching default may be absent from the comment: WordPress will restore them from the schema. Therefore, changing default will also affect older instances where the value was not saved explicitly.

To output such markup from a PHP template, use do_blocks():

echo do_blocks( '<!-- wp:kama/notice {"message":"Site updated"} /-->' );

This is useful in a classic theme. In a block template, the block comment itself can be placed directly in the HTML file of the template.

HTML for an automatically registered block is not saved as a backup copy. After disabling the plugin, its dynamic output will disappear, even though the comment with attributes will remain in the post content.

Total

Creating your own Gutenberg blocks in PHP lets you expand the WordPress editor without React, npm builds, and separate JavaScript registration. The register_block_type() function and supports.autoRegister configuration enable adding a dynamic block, automatically creating attribute fields, and server-side previews. Metadata can be defined in a PHP array or block.json, and HTML can be generated via render_callback or the render.php template. This approach is suitable for developing simple custom blocks: notifications, post lists, cards, and outputting meta-fields.