Building an Advanced Heading 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.


WordPress ships a perfectly capable Heading block. So why build another one? Because the core block deliberately avoids opinionated decorative styles — its job is semantics and typography, not visual flair. An Advanced Heading block can offer curated, pre-designed styles (think underlines, accent bars, grid-line dividers, border boxes) that a design-system-minded developer can maintain in CSS while editors simply pick a style from the dropdown.

This block has 13 decorative styles. All are driven by CSS pseudo-elements (::before and ::after) that use currentColor for their accent decoration, which means they automatically pick up whatever colour the editor has chosen via the native Text Color panel. No custom colour picker to wire up — the block supports system handles it.


What the block does

  • A SelectControl for heading level (H1–H6)
  • A SelectControl for decorative style (1–13)
  • An optional tagline (rendered as a <span> inside the heading element)
  • Native colour pickers for text and background via supports.color
  • Full typography panel: font size, weight, style, line-height, letter-spacing, text transform, text decoration — all via supports.typography
  • Margin and padding via supports.spacing
  • Border colour, radius, style, and width via supports.border

The mini-plugin

Create advanced-heading-block/ in wp-content/plugins/:

advanced-heading-block/advanced-heading-block.php

<?php
/**
 * Plugin Name: Advanced Heading Block
 * Description: A PHP-only Advanced Heading Gutenberg block with 13 decorative styles.
 * Version:     1.0.0
 * Requires at least: 6.7
 * Requires Plugins: gutenberg
 */

defined( 'ABSPATH' ) || exit;

define( 'ADV_HEADING_VERSION', '1.0.0' );

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

add_action( 'init', 'adv_heading_block_register' );

function adv_heading_block_register(): void {
    register_block_type(
        'my-plugin/advanced-heading',
        array(
            'title'       => 'Advanced Heading',
            'description' => 'A heading with 13 decorative styles. Accent decorations follow the Text Color.',
            'category'    => 'text',
            'icon'        => 'heading',

            'attributes' => array(
                'text' => array(
                    'type'    => 'string',
                    'default' => 'Your Heading',
                    'label'   => 'Text',
                ),
                'tagline' => array(
                    'type'    => 'string',
                    'default' => '',
                    'label'   => 'Tagline',
                ),
                'level' => array(
                    'type'    => 'string',
                    'enum'    => array( 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' ),
                    'default' => 'h2',
                    'label'   => 'Level',
                ),
                // Named 'headingStyle' (not 'style') to avoid collision with
                // the implicit 'style' object attribute added by block supports.
                'headingStyle' => array(
                    'type'    => 'string',
                    'enum'    => array( '1','2','3','4','5','6','7','8','9','10','11','12','13' ),
                    'default' => '1',
                    'label'   => 'Heading Style',
                ),
            ),

            'supports' => array(
                'autoRegister' => true,
                'align'        => array( 'wide', 'full' ),
                'color'        => array(
                    'text'       => true,
                    'background' => true,
                ),
                // All seven typography sub-options. WordPress applies these as
                // inline styles on the wrapper via get_block_wrapper_attributes(),
                // and they cascade to the inner heading element through CSS
                // inheritance. Do NOT hardcode font-size/weight/line-height in
                // your CSS, or these controls will appear to have no effect.
                'typography'   => array(
                    'fontSize'       => true,
                    'lineHeight'     => true,
                    'fontWeight'     => true,
                    'fontStyle'      => true,
                    'letterSpacing'  => true,
                    'textTransform'  => true,
                    'textDecoration' => true,
                ),
                'spacing'      => array(
                    'margin'  => true,
                    'padding' => true,
                ),
                // Border double-rendering in the editor was fixed in Gutenberg
                // #72039 (skipBlockSupportAttributes for autoRegister blocks).
                // Requires Gutenberg 21.9+ or WordPress 7.0+.
                'border'       => array(
                    'color'  => true,
                    'radius' => true,
                    'style'  => true,
                    'width'  => true,
                ),
            ),

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

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

function adv_heading_block_render( array $attributes, string $content, WP_Block $block ): string {
    $allowed   = array( 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' );
    $level     = in_array( $attributes['level'], $allowed, true ) ? $attributes['level'] : 'h2';
    $style_num = absint( $attributes['headingStyle'] );

    if ( $style_num < 1 || $style_num > 13 ) {
        $style_num = 1;
    }

    // Colour support is applied by get_block_wrapper_attributes(). The CSS uses
    // var(--adv-heading-accent, currentColor) for pseudo-element decorations, so
    // they automatically follow the Text Color chosen by the editor.
    $wrapper_attrs = get_block_wrapper_attributes(
        array( 'class' => 'adv-heading-wrap adv-heading--style-' . $style_num )
    );

    $tagline_html = '';
    if ( ! empty( $attributes['tagline'] ) ) {
        $tagline_html = '<span class="adv-heading__tagline">' . esc_html( $attributes['tagline'] ) . '</span>';
    }

    return sprintf(
        '<div %1$s><%2$s class="adv-heading">%3$s%4$s</%2$s></div>',
        $wrapper_attrs,
        $level,
        esc_html( $attributes['text'] ),
        $tagline_html
    );
}

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

add_action( 'init', 'adv_heading_block_register_styles' );

function adv_heading_block_register_styles(): void {
    wp_enqueue_block_style(
        'my-plugin/advanced-heading',
        array(
            'handle' => 'adv-heading-block-style',
            'src'    => plugin_dir_url( __FILE__ ) . 'style.css',
            'path'   => plugin_dir_path( __FILE__ ) . 'style.css',
            'ver'    => ADV_HEADING_VERSION,
        )
    );
}

add_action( 'enqueue_block_editor_assets', 'adv_heading_block_editor_styles' );

function adv_heading_block_editor_styles(): void {
    wp_enqueue_style(
        'adv-heading-block-style',
        plugin_dir_url( __FILE__ ) . 'style.css',
        array(),
        ADV_HEADING_VERSION
    );
}

advanced-heading-block/style.css

The CSS uses var(--adv-heading-accent, currentColor) throughout. If the editor sets a text colour, currentColor on the wrapper propagates it to all pseudo-elements automatically. No custom colour attribute needed.

/* Base */
.adv-heading-wrap { position: relative; }

.adv-heading {
    position: relative;
    margin: 0;
    padding: 0;
    transition: all 0.4s ease;
}

.adv-heading__tagline {
    display: block;
    font-size: 0.5em;
    line-height: 1.3;
}

/* Style 1 — centred with short accent bar below */
.adv-heading--style-1 .adv-heading {
    text-align: center;
    text-transform: uppercase;
    padding-bottom: 5px;
}
.adv-heading--style-1 .adv-heading:before {
    width: 28px; height: 5px;
    display: block; content: "";
    position: absolute; bottom: 3px;
    left: 50%; margin-left: -14px;
    background-color: var(--adv-heading-accent, currentColor);
}
.adv-heading--style-1 .adv-heading:after {
    width: 100px; height: 1px;
    display: block; content: "";
    position: relative; margin-top: 25px;
    left: 50%; margin-left: -50px;
    background-color: var(--adv-heading-accent, currentColor);
}

/* Style 2 — left accent line with small caps tagline */
.adv-heading--style-2 .adv-heading { text-transform: capitalize; }
.adv-heading--style-2 .adv-heading:before {
    position: absolute; left: 0; bottom: 0;
    width: 60px; height: 2px; content: "";
    background-color: var(--adv-heading-accent, currentColor);
}
.adv-heading--style-2 .adv-heading__tagline {
    font-size: 13px; font-weight: 500;
    text-transform: uppercase; letter-spacing: 4px;
    line-height: 3em; padding-left: 0.25em;
    color: rgba(0,0,0,0.4); padding-bottom: 10px;
}

/* Style 3 — thick + thin underline combo */
.adv-heading--style-3 .adv-heading {
    padding-bottom: 15px;
}
.adv-heading--style-3 .adv-heading:before {
    content: ""; position: absolute; left: 0; bottom: 0;
    height: 5px; width: 55px;
    background-color: var(--adv-heading-accent, currentColor);
}
.adv-heading--style-3 .adv-heading:after {
    content: ""; position: absolute; left: 0; bottom: 2px;
    height: 1px; width: 95%; max-width: 255px;
    background-color: var(--adv-heading-accent, currentColor);
}

/* Style 4 — centred with thin separator */
.adv-heading--style-4 .adv-heading {
    text-align: center; padding-bottom: 0.7em;
}
.adv-heading--style-4 .adv-heading__tagline {
    line-height: 2em; padding-bottom: 0.35em;
    color: rgba(0,0,0,0.5);
}
.adv-heading--style-4 .adv-heading:before {
    position: absolute; bottom: 0;
    width: 60px; height: 1px; content: "";
    left: 50%; margin-left: -30px;
    background-color: var(--adv-heading-accent, currentColor);
}

/* Style 5 — uppercase with italic serif tagline */
.adv-heading--style-5 .adv-heading {
    text-align: center;
    text-transform: uppercase;
    letter-spacing: 2px;
}
.adv-heading--style-5 .adv-heading__tagline {
    margin-top: 40px; text-transform: none;
    font-style: italic; color: #999;
}
.adv-heading--style-5 .adv-heading:before {
    position: absolute; bottom: 38px;
    width: 60px; height: 4px; content: "";
    left: 50%; margin-left: -30px;
    background-color: var(--adv-heading-accent, currentColor);
    opacity: 0.3;
}

/* Style 6 — centred with flanking bars */
.adv-heading--style-6 .adv-heading {
    text-align: center; text-transform: uppercase; letter-spacing: 2px;
}
.adv-heading--style-6 .adv-heading__tagline {
    line-height: 2em; padding-bottom: 15px;
    font-style: italic; color: #999;
}
.adv-heading--style-6 .adv-heading:after,
.adv-heading--style-6 .adv-heading:before {
    position: absolute; bottom: 0;
    width: 45px; height: 4px; content: "";
    right: 45px; margin: auto;
    background-color: var(--adv-heading-accent, currentColor);
    opacity: 0.4;
}
.adv-heading--style-6 .adv-heading:before {
    left: 45px; width: 90px; opacity: 0.7;
}

/* Style 7 — grid line flanks (left + right) */
.adv-heading--style-7 .adv-heading {
    text-align: center; text-transform: uppercase; letter-spacing: 1px;
    display: grid;
    grid-template-columns: 1fr max-content 1fr;
    grid-template-rows: 27px 0;
    gap: 20px;
    align-items: center;
}
.adv-heading--style-7 .adv-heading:after,
.adv-heading--style-7 .adv-heading:before {
    content: " "; display: block;
    border-bottom: 1px solid var(--adv-heading-accent, currentColor);
    border-top: 1px solid var(--adv-heading-accent, currentColor);
    height: 5px;
}

/* Style 8 — minimal grid line flanks */
.adv-heading--style-8 .adv-heading {
    text-align: center; text-transform: uppercase; letter-spacing: 1px;
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    grid-template-rows: 16px 0;
    gap: 22px;
}
.adv-heading--style-8 .adv-heading:after,
.adv-heading--style-8 .adv-heading:before {
    content: " "; display: block;
    border-bottom: 2px solid var(--adv-heading-accent, currentColor);
    opacity: 0.3;
}

/* Style 9 — Playfair Display style with flanked tagline */
.adv-heading--style-9 .adv-heading { text-align: center; }
.adv-heading--style-9 .adv-heading__tagline {
    margin-top: 5px; font-size: 15px; color: #444;
    word-spacing: 1px; letter-spacing: 2px; text-transform: uppercase;
    display: grid;
    grid-template-columns: 1fr max-content 1fr;
    grid-template-rows: 27px 0;
    gap: 20px;
    align-items: center;
}
.adv-heading--style-9 .adv-heading__tagline:after,
.adv-heading--style-9 .adv-heading__tagline:before {
    content: " "; display: block;
    border-bottom: 1px solid var(--adv-heading-accent, currentColor);
    border-top: 1px solid var(--adv-heading-accent, currentColor);
    height: 5px; opacity: 0.3;
}

/* Style 10 — accent bar above */
.adv-heading--style-10 .adv-heading { text-transform: uppercase; }
.adv-heading--style-10 .adv-heading:before {
    background-color: var(--adv-heading-accent, currentColor);
    border-radius: 0.25rem;
    content: ''; display: block;
    height: 0.25rem; width: 42px;
    margin-bottom: 1.25rem;
}

/* Style 11 — full-width rule with section-sign ornament */
.adv-heading--style-11 .adv-heading {
    text-align: center; padding-bottom: 45px;
    text-transform: uppercase; letter-spacing: 2px;
}
.adv-heading--style-11 .adv-heading:before {
    position: absolute; left: 0; bottom: 20px;
    width: 60%; left: 50%; margin-left: -30%;
    height: 1px; content: "";
    background-color: var(--adv-heading-accent, currentColor);
    opacity: 0.4;
}
.adv-heading--style-11 .adv-heading:after {
    position: absolute; width: 40px; height: 40px;
    left: 50%; margin-left: -20px; bottom: 0;
    content: '\00a7'; font-size: 30px; line-height: 40px;
    color: var(--adv-heading-accent, currentColor);
    font-weight: 400; display: block;
}

/* Style 12 — flanking bars top and bottom */
.adv-heading--style-12 .adv-heading {
    text-transform: uppercase; text-align: center; white-space: nowrap;
    padding-bottom: 13px;
}
.adv-heading--style-12 .adv-heading:before {
    background-color: var(--adv-heading-accent, currentColor);
    content: ''; display: block;
    height: 3px; width: 75px; margin-bottom: 5px;
}
.adv-heading--style-12 .adv-heading:after {
    background-color: var(--adv-heading-accent, currentColor);
    content: ''; display: block;
    position: absolute; right: 0; bottom: 0;
    height: 3px; width: 75px;
}

/* Style 13 — border box with dot ornaments */
.adv-heading--style-13 .adv-heading {
    text-transform: uppercase; text-align: center; white-space: nowrap;
    border: 2px solid currentColor;
    padding: 5px 11px 3px;
}
.adv-heading--style-13 .adv-heading:before,
.adv-heading--style-13 .adv-heading:after {
    background-color: var(--adv-heading-accent, currentColor);
    position: absolute; content: '';
    height: 7px; width: 7px; border-radius: 50%;
    bottom: 12px;
}
.adv-heading--style-13 .adv-heading:before { left: -20px; }
.adv-heading--style-13 .adv-heading:after  { right: -20px; }

Things to know

Don’t hardcode typography in CSS

This is the most common mistake when adapting a design that already has font-size, font-weight, and line-height hardcoded. If your CSS sets:

.adv-heading { font-size: 40px; font-weight: 300; }

…the Typography panel in the editor will appear to have no effect. WordPress applies typography block supports as inline styles on the wrapper element. Those inline styles cascade to child elements — but only if the CSS isn’t overriding them with its own static values.

Remove all hardcoded font-size, font-weight, and line-height from the heading element. Leave them only on sub-elements that have intentional fixed sizing (like the tagline in style 9, or the ::after ornament in style 11).

The currentColor accent trick

Every pseudo-element decoration uses var(--adv-heading-accent, currentColor). The --adv-heading-accent custom property is never set in the CSS — so the fallback, currentColor, is always used. And currentColor is the inherited color value from the parent.

When the editor sets a Text Color via supports.color.text, that colour is written as color: #xxx on the wrapper div (via get_block_wrapper_attributes()). From there, it cascades down to the heading element, and then currentColor in the pseudo-elements picks it up. Accent decorations automatically match the heading text colour without any extra code.

Block supports and inline styles

supports.typography, supports.spacing, and supports.border all write their values as inline styles on the wrapper element (the outer <div>). They cascade into the inner heading element through CSS inheritance, but some properties like border apply to the wrapper itself. This is intentional — it means the editor can add padding and a border around the heading block as a design element.


Further reading

Related Posts

Leave a Reply

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