Reference for building Gutenberg blocks using PHP only — no JavaScript build step, no React, no Node.js. Requires: WordPress 7.0+ or Gutenberg plugin 21.8+
Author: (ajithrn)
Add 'autoRegister' => true to the supports array in register_block_type(). WordPress auto-generates editor sidebar controls from your attributes and uses ServerSideRender for the editor preview.
Flow: Change attribute in sidebar → REST API call → PHP render_callback → updated preview.
add_action('init', function () {
register_block_type('myplugin/hello', [
'title' => 'Hello Block',
'category' => 'widgets',
'icon' => 'smiley',
'render_callback' => function ($attributes) {
$wrapper = get_block_wrapper_attributes();
return sprintf('<p %s>%s</p>', $wrapper, esc_html($attributes['message']));
},
'attributes' => [
'message' => [
'type' => 'string',
'default' => 'Hello, world!',
'label' => 'Message',
],
],
'supports' => [
'autoRegister' => true,
],
]);
});That's a fully functional block. No block.json, no editor.js, no build step.
WordPress inspects the attributes array and generates sidebar controls:
| Attribute Type | Generated Control |
|---|---|
'type' => 'string' |
TextControl |
'type' => 'string' + 'enum' => [...] |
SelectControl |
'type' => 'integer' or 'number' |
NumberControl |
'type' => 'boolean' |
ToggleControl |
- Attributes with
'role' => 'local' - Types:
object,array - Reserved names:
style,className,textColor,backgroundColor,fontSize,fontFamily,align,anchor
'buttonUrl' => [
'type' => 'string',
'default' => '#',
'label' => 'Button URL',
],Without label, WordPress derives it from the attribute name (camelCase → "Camel Case").
Values are used as option labels directly — use sentence case:
'speed' => [
'type' => 'string',
'enum' => ['Slow', 'Medium', 'Fast'],
'default' => 'Medium',
'label' => 'Speed',
],The supports array accepts the same keys as JS-registered blocks. get_block_wrapper_attributes() handles all output automatically.
'supports' => [
'autoRegister' => true,
'align' => ['wide', 'full'],
'color' => ['text' => true, 'background' => true],
'typography' => [
'fontSize' => true,
'lineHeight' => true,
'fontWeight' => true,
'letterSpacing' => true,
],
'spacing' => ['margin' => true, 'padding' => true],
'border' => ['color' => true, 'radius' => true, 'style' => true, 'width' => true],
],These unlock native Color, Typography, Spacing, and Border panels in the sidebar — no extra PHP needed.
Always use get_block_wrapper_attributes() for the outer element:
'render_callback' => function ($attributes) {
$wrapper = get_block_wrapper_attributes([
'class' => 'my-block my-block--' . esc_attr($attributes['variant']),
]);
return sprintf('<div %s>...</div>', $wrapper);
},wp_enqueue_block_style('myplugin/my-block', [
'handle' => 'myplugin-my-block',
'src' => get_theme_file_uri('blocks/my-block/style.css'),
'path' => get_theme_file_path('blocks/my-block/style.css'),
'ver' => '1.0.0',
]);add_action('enqueue_block_editor_assets', function () {
wp_enqueue_style(
'myplugin-my-block',
get_theme_file_uri('blocks/my-block/style.css'),
[],
'1.0.0'
);
});Block supports add an implicit style attribute (type object). Naming yours style causes:
Error loading block: Invalid parameter(s): attributes
Use variant, theme, layout, etc. instead.
Full list of reserved attribute names: style, className, textColor, backgroundColor, gradient, fontSize, fontFamily, align, anchor
For pseudo-elements that need a color beyond text/background:
$accent = !empty($attributes['accentColor'])
? '--accent:' . esc_attr($attributes['accentColor']) . ';'
: '';
$wrapper = get_block_wrapper_attributes([
'class' => 'my-block',
'style' => $accent,
]);.my-block::before {
background-color: var(--accent, currentColor);
}Register at init, enqueue from render callback:
add_action('init', function () {
wp_register_script('my-block-script', get_theme_file_uri('blocks/my-block/frontend.js'), [], '1.0.0', true);
});
// In render_callback — only loads on pages with the block
wp_enqueue_script('my-block-script');| Limitation | Workaround |
|---|---|
| No InnerBlocks | Use JSX block registration for nested content |
| No media/file upload controls | Use JSX or store media ID as integer attribute |
| No rich-text inline editing | Use JSX for blocks needing inline text editing |
| Editor preview not live-reactive | Attribute changes trigger server round-trip |
wp_enqueue_block_style() doesn't work in editor |
Use enqueue_block_editor_assets hook |
| Use PHP-Only | Use JSX (build step) |
|---|---|
| CTA banners, notices, cards | Hero with media upload |
| Headings with decorative styles | Blocks with InnerBlocks |
| Author boxes, team cards | Image galleries with drag-drop |
| Stats counters, marquees | Blocks needing live preview |
| Any block with text/select/toggle/number controls | Blocks with rich-text inline editing |