From Shortcode to Block: Modernizing Legacy Features in WordPress

October 7, 2025
From Shortcode to Block: Modernizing Legacy Features in WordPress

From Shortcode to Block: Modernizing Legacy Features in WordPress

Shortcodes have powered dynamic content in WordPress for over a decade, but as the Block Editor (Gutenberg) matures, modern development is shifting toward blocks. Blocks provide a visual, user-friendly editing experience that shortcodes can’t match. In this article, you’ll learn how to migrate your legacy shortcodes into reusable, editor-native blocks — without losing backward compatibility.

Why Move from Shortcodes to Blocks?

Shortcodes are powerful but invisible to users. They clutter the editor with code-like tags and make it hard to see how content will look on the front end. Blocks solve this by offering live previews, field controls, and standardized APIs.

Benefits of Converting Shortcodes to Blocks

  • Visual editing: Users can see the final layout inside the editor.
  • Structured input: Blocks use fields instead of shortcode attributes.
  • Reusable logic: Register blocks and render them dynamically with PHP or JavaScript.
  • Future-proof: Aligns with WordPress’s long-term direction (Gutenberg-first development).

Step 1: Identify a Legacy Shortcode

Let’s assume you already have a shortcode like this:

<?php
// functions.php
add_shortcode( 'button', function ( $atts, $content = null ) {
    $atts = shortcode_atts( array(
        'url'   => '#',
        'color' => 'blue',
    ), $atts, 'button' );

    $url   = esc_url( $atts['url'] );
    $color = esc_attr( $atts['color'] );
    $text  = wp_kses_post( $content );

    return '<a href="' . $url . '" class="btn btn-' . $color . '">' . $text . '</a>';
} );

Usage example in the Classic Editor:

[button url="https://example.com" color="green"]Buy Now[/button]

Step 2: Register a Block that Uses the Same Logic

You can modernize this shortcode into a block while reusing the same rendering callback. WordPress provides register_block_type() for dynamic (server-rendered) blocks.

<?php
// functions.php
add_action( 'init', function () {

    register_block_type( 'wpt/button', array(
        'api_version' => 2,
        'title'       => __( 'Button', 'your-textdomain' ),
        'icon'        => 'button',
        'category'    => 'design',
        'attributes'  => array(
            'url' => array(
                'type' => 'string',
                'default' => '#',
            ),
            'color' => array(
                'type' => 'string',
                'default' => 'blue',
            ),
            'text' => array(
                'type' => 'string',
                'default' => 'Click Me',
            ),
        ),
        'render_callback' => 'wpt_render_button_block',
    ) );
} );

function wpt_render_button_block( $attributes ) {
    $url   = esc_url( $attributes['url'] );
    $color = esc_attr( $attributes['color'] );
    $text  = esc_html( $attributes['text'] );

    return '<a href="' . $url . '" class="btn btn-' . $color . '">' . $text . '</a>';
}

This approach ensures you can update styling or logic in one place and apply it to both shortcode and block versions.

Step 3: Create a Block Editor Script (Optional Visual Controls)

For richer block controls, enqueue a JavaScript file that defines the block’s editor UI. Example using registerBlockType from @wordpress/blocks:

// file: /blocks/button/edit.js
import { __ } from '@wordpress/i18n';
import { useBlockProps } from '@wordpress/block-editor';
import { TextControl, SelectControl, URLInputButton } from '@wordpress/components';

export default function Edit( { attributes, setAttributes } ) {
  const { url, color, text } = attributes;
  const blockProps = useBlockProps();

  return (
    <div {...blockProps}>
      <TextControl
        label={ __( 'Button Text', 'your-textdomain' ) }
        value={ text }
        onChange={ ( value ) => setAttributes( { text: value } ) }
      />
      <URLInputButton
        label={ __( 'Button Link', 'your-textdomain' ) }
        url={ url }
        onChange={ ( value ) => setAttributes( { url: value } ) }
      />
      <SelectControl
        label={ __( 'Button Color', 'your-textdomain' ) }
        value={ color }
        options={[
          { label: 'Blue', value: 'blue' },
          { label: 'Green', value: 'green' },
          { label: 'Red', value: 'red' },
        ]}
        onChange={ ( value ) => setAttributes( { color: value } ) }
      />
    </div>
  );
}

Then register this editor script in PHP:

<?php
add_action( 'init', function () {
    wp_register_script(
        'wpt-button-block-editor',
        get_template_directory_uri() . '/blocks/button/edit.js',
        array( 'wp-blocks', 'wp-element', 'wp-editor', 'wp-components' ),
        '1.0.0',
        true
    );

    register_block_type( 'wpt/button', array(
        'editor_script' => 'wpt-button-block-editor',
        'render_callback' => 'wpt_render_button_block',
        'attributes'     => array(
            'url' => array( 'type' => 'string', 'default' => '#' ),
            'color' => array( 'type' => 'string', 'default' => 'blue' ),
            'text' => array( 'type' => 'string', 'default' => 'Click Me' ),
        ),
    ) );
} );

Step 4: Maintain Backward Compatibility

If your site already has old shortcodes like [button], you can keep them working by simply reusing the same rendering function for both systems:

<?php
add_shortcode( 'button', function ( $atts, $content = null ) {
    $atts = shortcode_atts( array(
        'url'   => '#',
        'color' => 'blue',
    ), $atts, 'button' );

    // Reuse the same logic as the block renderer
    return wpt_render_button_block( array(
        'url'   => $atts['url'],
        'color' => $atts['color'],
        'text'  => $content ?: 'Click Me',
    ) );
} );

Step 5: Test in the Editor

  • Search for your new “Button” block in the editor.
  • Set link, color, and label values from the sidebar.
  • Save and verify the front-end matches shortcode output.

When to Use Dynamic vs Static Blocks

  • Dynamic Blocks: PHP-rendered blocks that use render_callback — best for data that may change (like shortcodes, queries, or user info).
  • Static Blocks: Fully saved as HTML — best for simple markup and design-based components.

Tips for Migrating Shortcodes

  • Keep your shortcode active while introducing the block to avoid breaking old content.
  • Match attribute names between shortcodes and block attributes for easier migration.
  • Use wp.blocks.registerBlockType() or block.json to define metadata consistently.
  • Gradually phase out shortcode documentation once the block version is stable.

Conclusion

Converting shortcodes into modern WordPress blocks lets you preserve legacy logic while improving user experience. Start with a dynamic block using register_block_type() and reuse your shortcode’s render function. You’ll gain visual editing, structured attributes, and a future-proof way to manage content in the Block Editor.

Shortcodes belong to the past — blocks are the future of WordPress development.

Avatar

Written by

satoshi

I’ve been building and customizing WordPress themes for over 10 years. In my free time, you’ll probably find me enjoying a good football match.