WordPress Gutenberg registerBlockVariation: Create Pre-Configured Block Presets for the Inserter

Block variations are a way to create pre-configured versions of an existing block that appear as distinct entries in the block inserter, complete with their own name, description, icon, and default attribute values. They are lighter-weight than a full custom block registration — no PHP registration needed, no new save/edit functions — yet they give users a distinct block identity in the inserter. WordPress itself uses block variations extensively: the Columns block, the Embed block (each embed provider is a variation of core/embed), and the Social Links block all rely on variations. Custom variations are useful for: creating brand-specific heading styles that are pre-configured, defining standard column layouts, or registering site-specific embed providers. The registerBlockVariation function is the JavaScript API for this, and unregisterBlockVariation allows removing both custom and core variations.

Problem: A corporate site needs three pre-configured core/group variations — "Hero Section", "Feature Strip", and "CTA Block" — with specific background colours, padding, and alignment pre-set, so content editors can insert them with one click rather than manually configuring each Group block.

Solution: Register block variations with registerBlockVariation in an enqueue_block_editor_assets script. Each variation sets the attributes and innerBlocks defaults that will be applied when the variation is inserted.

// variations.js — loaded via enqueue_block_editor_assets
const { registerBlockVariation, unregisterBlockVariation } = wp.blocks;

// ── Register a "Hero Section" variation of core/group ─────────────────
registerBlockVariation( 'core/group', {
    name:        'my-theme/hero-section',      // unique identifier
    title:       'Hero Section',
    description: 'Full-width group with dark background for hero content.',
    icon:        'cover-image',                // dashicon slug or SVG element
    keywords:    [ 'hero', 'banner', 'header' ],
    isDefault:   false,
    scope:       [ 'inserter' ],              // show in inserter only (not transforms)

    // Default attributes applied when inserted
    attributes: {
        align:           'full',
        style: {
            color:   { background: '#0a2342' },
            spacing: { padding: { top: '80px', bottom: '80px' } },
        },
    },

    // Inner blocks pre-inserted with the variation
    innerBlocks: [
        [ 'core/heading',   { level: 1, textAlign: 'center', placeholder: 'Hero Heading' } ],
        [ 'core/paragraph', { align: 'center', placeholder: 'Hero subheading text…' } ],
        [ 'core/buttons',   { layout: { type: 'flex', justifyContent: 'center' } }, [
            [ 'core/button', { text: 'Get Started' } ],
        ] ],
    ],
} );

registerBlockVariation( 'core/group', {
    name:  'my-theme/cta-block',
    title: 'CTA Block',
    icon:  'megaphone',
    attributes: {
        align: 'wide',
        style: { color: { background: '#f5c842' }, spacing: { padding: { top: '40px', bottom: '40px' } } },
    },
    innerBlocks: [
        [ 'core/heading',   { level: 2, textAlign: 'center', placeholder: 'Call to Action' } ],
        [ 'core/buttons',   {}, [ [ 'core/button', { text: 'Learn More' } ] ] ],
    ],
} );

// ── Remove a core variation (e.g. hide the "Spotify" embed variation) ──
wp.domReady( function () {
    unregisterBlockVariation( 'core/embed', 'spotify' );
} );

// ── isActive callback: identify which variation is currently applied ───
registerBlockVariation( 'core/heading', {
    name:       'my-theme/section-title',
    title:      'Section Title',
    attributes: { level: 2, className: 'section-title' },
    // isActive: called to highlight the variation in the toolbar
    isActive: ( blockAttributes ) =>
        blockAttributes.className === 'section-title',
} );

<?php
// Enqueue the variations script in the block editor
add_action( 'enqueue_block_editor_assets', function () {
    wp_enqueue_script(
        'my-theme-block-variations',
        get_stylesheet_directory_uri() . '/js/variations.js',
        [ 'wp-blocks', 'wp-dom-ready', 'wp-element' ],
        wp_get_theme()->get( 'Version' ),
        true
    );
} );

NOTE: The isActive callback is important for variations that only differ in attribute values — without it, WordPress cannot tell which variation is active in the toolbar and will show the generic parent block name. It receives the block's current attributes and inner blocks as arguments and should return true when the block matches this variation. The scope array controls where the variation appears: 'inserter' (block inserter), 'transform' (transform menu), and 'block' (block-level UI). The default is all three. Set 'scope': ['inserter'] to keep the transform menu clean. Block variations are saved in the post content as the parent block type (core/group) with the variation's attributes — they are not a separate block type and do not require a separate register_block_type().