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
SelectControlgenerated automatically from a stringenum - 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 type | Generated control |
|---|---|
string | TextControl |
string with enum | SelectControl |
integer | NumberControl |
boolean | ToggleControl |
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
- PHP-Only Block Registration in WordPress
- Make WordPress Core dev note
- Block Editor Handbook — Registration of a block