WordPress Gutenberg Block Transforms: Convert Between Blocks with transforms.from and to

Gutenberg’s block transforms system allows one block to be converted into another without losing content. This is what lets you right-click a Paragraph block and choose “Transform to → Heading”, or select multiple Image blocks and merge them into a Gallery. Transforms are defined in a block’s JavaScript registration object under the transforms property as two arrays: from defines how other entities — other block types, raw HTML, shortcodes, or file drops — become this block; to defines what this block can be transformed into. Supporting transforms is a quality-of-life feature that makes custom blocks feel like first-class Gutenberg citizens. An editor who pasted a Paragraph and then realizes it should be a custom Quote Card block should be able to transform without re-entering content, and should be able to transform it back without losing text.

Problem: Your custom my-plugin/callout block has a text attribute, but editors can't convert an existing Paragraph into a Callout, or convert a Callout back to a Paragraph — they have to delete and re-create the block instead.

Solution: Add a transforms property to the block registration with from and to entries mapping between core/paragraph and your block using the block transform type.

import { registerBlockType } from '@wordpress/blocks';

registerBlockType( 'my-plugin/callout', {
    title: 'Callout',
    attributes: {
        text:  { type: 'string', default: '' },
        style: { type: 'string', default: 'info' }, // 'info' | 'warning' | 'success'
    },

    transforms: {
        from: [
            {
                // Convert core/paragraph → my-plugin/callout
                type:   'block',
                blocks: [ 'core/paragraph' ],
                transform: ( { content } ) => {
                    return createBlock( 'my-plugin/callout', {
                        text:  content,  // paragraph content becomes callout text
                        style: 'info',   // default style
                    } );
                },
            },
            {
                // Convert multiple paragraphs into one callout (merged content)
                type:       'block',
                blocks:     [ 'core/paragraph' ],
                isMultiBlock: true,
                transform: ( paragraphs ) => {
                    const combined = paragraphs.map( p => p.content ).join( '<br>' );
                    return createBlock( 'my-plugin/callout', { text: combined } );
                },
            },
        ],
        to: [
            {
                // Convert my-plugin/callout → core/paragraph
                type:   'block',
                blocks: [ 'core/paragraph' ],
                transform: ( { text } ) => {
                    return createBlock( 'core/paragraph', { content: text } );
                },
            },
            {
                // Convert my-plugin/callout → core/heading
                type:   'block',
                blocks: [ 'core/heading' ],
                transform: ( { text } ) => {
                    return createBlock( 'core/heading', { content: text, level: 3 } );
                },
            },
        ],
    },

    edit: ( { attributes, setAttributes } ) => { /* ... */ },
    save: ( { attributes } ) => { /* ... */ },
} );

NOTE: The createBlock function must be imported from @wordpress/blocks. The transform callback for from.block receives the source block's attributes and must return a new block object (created with createBlock) — it does not return raw HTML. For the to direction, the callback receives your block's attributes and must return the target block object. You can also define shortcode transforms (type: 'shortcode'), enter transforms triggered by a keyboard prefix (type: 'enter'), and raw HTML transforms (type: 'raw') for migrating classic content.