Building a Gutenberg Sidebar Plugin with WordPress Plugin API

Gutenberg exposes a JavaScript plugin API that lets you register custom sidebars (also called PluginSidebar) that appear in the block editor’s right panel alongside the existing Document and Block tabs. This is the recommended way to add meta-box-style UI without leaving the block editor.

Problem: The WordPress plugin API offers register_plugin_sidebar() for editor sidebars, but wiring together React components, block attributes, post meta, REST API endpoints, and PluginSidebar into a cohesive data flow is not well-documented.

Solution: Register a plugin sidebar with registerPlugin() + PluginSidebar from @wordpress/editor. Read and write post meta through the @wordpress/data store using useSelect and useDispatch with editPost. Ensure meta is registered server-side with show_in_rest => true and auth_callback.


The example below registers a sidebar with a ToggleControl and a TextControl that read and write post meta via the useEntityProp hook, and enqueues the script only on the editor screen.


// functions.php
add_action( 'enqueue_block_editor_assets', function () {
    wp_enqueue_script(
        'my-sidebar-plugin',
        get_theme_file_uri( 'js/sidebar-plugin.js' ),
        [ 'wp-plugins', 'wp-edit-post', 'wp-element',
          'wp-components', 'wp-data', 'wp-core-data' ],
        '1.0.0',
        true
    );
} );

// Register a meta field so the REST API exposes it
add_action( 'init', function () {
    register_post_meta( 'post', '_featured_badge', [
        'show_in_rest' => true,
        'single'       => true,
        'type'         => 'string',
        'auth_callback' => fn() => current_user_can( 'edit_posts' ),
    ] );
} );


// js/sidebar-plugin.js
const { registerPlugin } = wp.plugins;
const { PluginSidebar, PluginSidebarMoreMenuItem } = wp.editPost;
const { PanelBody, TextControl, ToggleControl } = wp.components;
const { useEntityProp } = wp.coreData;
const { useSelect } = wp.data;
const { Fragment } = wp.element;

function MyMetaSidebar() {
    const postType = useSelect( s => s('core/editor').getCurrentPostType() );
    const [ meta, setMeta ] = useEntityProp( 'postType', postType, 'meta' );

    const badge   = meta?._featured_badge ?? '';
    const enabled = badge !== '';

    return (
        wp.element.createElement( Fragment, null,
            wp.element.createElement( PluginSidebarMoreMenuItem,
                { target: 'my-meta-sidebar' }, 'Featured Badge'
            ),
            wp.element.createElement( PluginSidebar,
                { name: 'my-meta-sidebar', title: 'Featured Badge', icon: 'star-filled' },
                wp.element.createElement( PanelBody, { title: 'Badge Settings', initialOpen: true },
                    wp.element.createElement( ToggleControl, {
                        label: 'Show badge',
                        checked: enabled,
                        onChange: v => setMeta({ ...meta, _featured_badge: v ? 'Featured' : '' }),
                    } ),
                    enabled && wp.element.createElement( TextControl, {
                        label: 'Badge text',
                        value: badge,
                        onChange: v => setMeta({ ...meta, _featured_badge: v }),
                    } )
                )
            )
        )
    );
}

registerPlugin( 'my-meta-sidebar', { render: MyMetaSidebar } );


NOTE: Always register custom meta with auth_callback and show_in_rest => true; without auth_callback, any logged-in user could update the field via the REST API.