Skip to content

Instantly share code, notes, and snippets.

@ajithrn
Created May 19, 2026 09:09
Show Gist options
  • Select an option

  • Save ajithrn/d42948a45c3036de8c8f5a9e6e660e00 to your computer and use it in GitHub Desktop.

Select an option

Save ajithrn/d42948a45c3036de8c8f5a9e6e660e00 to your computer and use it in GitHub Desktop.
PHP-Only Block Registration Guide (WordPress 7.0+)

PHP-Only Block Registration Guide (WordPress 7.0+)

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)


How It Works

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.


Minimal Example

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.


Auto-Generated Controls

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

Controls NOT generated for:

  • Attributes with 'role' => 'local'
  • Types: object, array
  • Reserved names: style, className, textColor, backgroundColor, fontSize, fontFamily, align, anchor

Custom Labels

'buttonUrl' => [
    'type'    => 'string',
    'default' => '#',
    'label'   => 'Button URL',
],

Without label, WordPress derives it from the attribute name (camelCase → "Camel Case").

Enum Values (SelectControl)

Values are used as option labels directly — use sentence case:

'speed' => [
    'type'    => 'string',
    'enum'    => ['Slow', 'Medium', 'Fast'],
    'default' => 'Medium',
    'label'   => 'Speed',
],

Block Supports

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.


Render Callback Pattern

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);
},

Styling

Frontend (loads only when block is used)

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',
]);

Editor (workaround — autoRegister blocks need this)

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'
    );
});

Critical Gotchas

1. Never name an attribute style

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

2. CSS Custom Properties for accent colors

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);
}

3. Lazy-loading frontend JavaScript

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');

Known Limitations

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

When to Use PHP-Only vs JSX

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

References

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment