Building a Call to Action Block with PHP-Only Block Registration

on in WordPress | Last modified on

This article is part of a series on PHP-only Gutenberg block registration. For an introduction to the feature and its background, read PHP-Only Block Registration in WordPress.


A Call to Action (CTA) banner is one of the most common components on any marketing site. It typically contains a headline, a short supporting sentence, and a button. Traditionally, building this as a Gutenberg block meant writing JavaScript — JSX, registerBlockType, edit and save functions, a build step, the works.

With the autoRegister block support introduced in Gutenberg 21.8 (landing in WordPress 7.0), none of that is needed. You register the block entirely in PHP, WordPress auto-generates the editor controls, and the block renders server-side on the frontend. The whole thing is a single PHP file.


What the block does

  • Editable heading, subheading, button label, and button URL
  • A variant selector (Primary / Secondary / Dark) — a SelectControl generated automatically from a string enum
  • A toggle to open the button in a new tab
  • Native background and text colour pickers via supports.color
  • Wide and full alignment support

The mini-plugin

Create a folder called cta-block in your wp-content/plugins/ directory, then add one file:

cta-block/cta-block.php

<?php
/**
 * Plugin Name: CTA Block
 * Description: A PHP-only Call to Action Gutenberg block.
 * Version:     1.0.0
 * Requires at least: 6.7
 * Requires Plugins: gutenberg
 */

defined( 'ABSPATH' ) || exit;

define( 'CTA_BLOCK_VERSION', '1.0.0' );

// ---------------------------------------------------------------------------
// Block registration
// ---------------------------------------------------------------------------

add_action( 'init', 'cta_block_register' );

function cta_block_register(): void {
    register_block_type(
        'my-plugin/cta',
        array(
            'title'       => 'Call to Action',
            'description' => 'A customisable call-to-action banner.',
            'category'    => 'text',
            'icon'        => 'megaphone',

            'attributes' => array(
                'heading' => array(
                    'type'    => 'string',
                    'default' => 'Ready to get started?',
                    'label'   => 'Heading',
                ),
                'subheading' => array(
                    'type'    => 'string',
                    'default' => 'Join thousands of happy customers today.',
                    'label'   => 'Subheading',
                ),
                'buttonLabel' => array(
                    'type'    => 'string',
                    'default' => 'Get Started',
                    'label'   => 'Button Label',
                ),
                'buttonUrl' => array(
                    'type'    => 'string',
                    'default' => '#',
                    'label'   => 'Button URL',
                ),

                // Named 'variant', NOT 'style'. The block supports system adds
                // an implicit 'style' attribute (a nested object) for spacing,
                // border, and custom colour values. If you name your own
                // attribute 'style', the REST API will reject attribute changes
                // with "Invalid parameter(s): attributes". Use any other name.
                'variant' => array(
                    'type'    => 'string',
                    'enum'    => array( 'primary', 'secondary', 'dark' ),
                    'default' => 'primary',
                    'label'   => 'Variant',
                ),

                'openInNewTab' => array(
                    'type'    => 'boolean',
                    'default' => false,
                    'label'   => 'Open in New Tab',
                ),
            ),

            'supports' => array(
                'autoRegister' => true,
                'align'        => array( 'wide', 'full' ),
                'color'        => array(
                    'background' => true,
                    'text'       => true,
                ),
            ),

            'render_callback' => 'cta_block_render',
        )
    );
}

// ---------------------------------------------------------------------------
// Render callback
// ---------------------------------------------------------------------------

function cta_block_render( array $attributes, string $content, WP_Block $block ): string {
    // get_block_wrapper_attributes() merges any colour chosen via the native
    // Color panel (as a has-{color}-background-color class for palette colours,
    // or inline style for custom hex values).
    $wrapper_attrs = get_block_wrapper_attributes(
        array( 'class' => 'cta-block cta-block--' . esc_attr( $attributes['variant'] ) )
    );

    $target = $attributes['openInNewTab'] ? ' target="_blank" rel="noopener noreferrer"' : '';

    return sprintf(
        '<div %1$s>
            <h2 class="cta-block__heading">%2$s</h2>
            <p class="cta-block__subheading">%3$s</p>
            <a class="cta-block__button" href="%4$s"%5$s>%6$s</a>
        </div>',
        $wrapper_attrs,
        esc_html( $attributes['heading'] ),
        esc_html( $attributes['subheading'] ),
        esc_url( $attributes['buttonUrl'] ),
        $target,
        esc_html( $attributes['buttonLabel'] )
    );
}

// ---------------------------------------------------------------------------
// Styles
// ---------------------------------------------------------------------------

// Load on the frontend — only on pages containing the block.
add_action( 'init', 'cta_block_register_styles' );

function cta_block_register_styles(): void {
    wp_enqueue_block_style(
        'my-plugin/cta',
        array(
            'handle' => 'cta-block-style',
            'src'    => plugin_dir_url( __FILE__ ) . 'style.css',
            'path'   => plugin_dir_path( __FILE__ ) . 'style.css',
            'ver'    => CTA_BLOCK_VERSION,
        )
    );
}

// Load in the editor — wp_enqueue_block_style() doesn't reliably inject
// styles for autoRegister blocks in the editor, so we use this hook instead.
add_action( 'enqueue_block_editor_assets', 'cta_block_editor_styles' );

function cta_block_editor_styles(): void {
    wp_enqueue_style(
        'cta-block-style',
        plugin_dir_url( __FILE__ ) . 'style.css',
        array(),
        CTA_BLOCK_VERSION
    );
}

cta-block/style.css

/* Base */
.cta-block {
    padding: 3rem 2rem;
    text-align: center;
    border-radius: 0.5rem;
}

.cta-block__heading {
    margin: 0 0 0.5rem;
    font-size: 1.75rem;
    font-weight: 700;
}

.cta-block__subheading {
    margin: 0 0 1.5rem;
    opacity: 0.85;
}

.cta-block__button {
    display: inline-block;
    padding: 0.75rem 2rem;
    border-radius: 0.375rem;
    font-weight: 600;
    text-decoration: none;
    transition: opacity 0.2s ease;
}

.cta-block__button:hover {
    opacity: 0.85;
}

/* Variants */
.cta-block--primary {
    background-color: #0073aa;
    color: #ffffff;
}

.cta-block--primary .cta-block__button {
    background-color: #ffffff;
    color: #0073aa;
}

.cta-block--secondary {
    background-color: #f0f0f1;
    color: #1d2327;
}

.cta-block--secondary .cta-block__button {
    background-color: #1d2327;
    color: #ffffff;
}

.cta-block--dark {
    background-color: #1d2327;
    color: #ffffff;
}

.cta-block--dark .cta-block__button {
    background-color: #0073aa;
    color: #ffffff;
}

Activate the plugin from Plugins → Installed Plugins. You’ll find the block in the inserter under the Text category.


Things to know

The style attribute naming collision

This is the most important gotcha with autoRegister blocks. When you enable supports.color, supports.spacing, or supports.border, WordPress adds an implicit style attribute to your block (a nested object holding custom colour values, padding values, etc.). If you define your own attribute and name it style, the REST API’s type-checker sees a conflict and returns:

Error loading block: Invalid parameter(s): attributes

The fix is simple: never name a custom attribute style. Use variant, theme, layout, or anything else. The same caution applies to backgroundColor, textColor, and className — these are all implicit attributes added by various block supports.

Auto-generated controls

autoRegister reads your attributes array and auto-generates sidebar controls:

Attribute typeGenerated control
stringTextControl
string with enumSelectControl
integerNumberControl
booleanToggleControl

The label key sets the visible label in the sidebar. Without it, WordPress derives a label from the attribute name (e.g. buttonLabel becomes “Button label”). Always set label explicitly for predictable, professional-looking controls.

Colour via supports.color

When a user picks a colour from the native Color panel, WordPress adds it to the wrapper element via get_block_wrapper_attributes() — either as a has-{slug}-color class (for theme palette colours) or as an inline style="color:…" (for custom hex values). You don’t need to read $attributes['textColor'] or $attributes['backgroundColor'] yourself; it’s all handled automatically.

To make the colour cascade to child elements like .cta-block__heading and .cta-block__subheading, write CSS that inherits from the parent:

.cta-block__heading,
.cta-block__subheading {
    color: inherit;
}

Further reading

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *