Add a Custom Sidebar Panel to the Gutenberg Editor with PluginSidebar

Gutenberg ships with a sidebar that shows two panels: Document (post settings, categories, tags) and Block (settings for the currently selected block). The block editor’s plugin API lets you add a third panel of your own — with any controls you choose — without touching the Document or Block panels or adding a classic meta box. This is useful for editorial workflows that need additional fields in the editor: reading-time estimates, internal notes, SEO scores, approval status flags, or any custom post meta. The mechanism is the PluginSidebar component from @wordpress/edit-post, combined with registerPlugin() from @wordpress/plugins. The sidebar entry also automatically appears in the editor’s three-dot Options menu under a Plugins section so the author can open or close it at any time. The underlying data — whatever you put in the sidebar — is stored as post meta and accessed through WordPress’s REST API data layer, which means you must register the meta field with register_post_meta() and set show_in_rest to true. All of this works without a build step: the example below uses wp.element.createElement (the WordPress-bundled version of React) rather than JSX, so it runs as a plain .js file enqueued with enqueue_block_editor_assets.

Problem: You need to expose additional fields — post meta, internal notes, or custom controls — in the Gutenberg editor without adding a classic meta box below the content area or cluttering the existing Document panel.

Solution: Register a custom plugin with registerPlugin() and render a PluginSidebar inside it. Expose the post meta field via register_post_meta() with show_in_rest: true so the JavaScript data layer can read and write it through the REST API.

Step 1 — enqueue the sidebar script only in the block editor (not on the front end):

<?php
add_action( 'enqueue_block_editor_assets', 'register_custom_sidebar_script' );

function register_custom_sidebar_script() {
    wp_enqueue_script(
        'my-custom-sidebar',
        get_template_directory_uri() . '/js/editor-sidebar.js',
        [ 'wp-plugins', 'wp-edit-post', 'wp-element', 'wp-components', 'wp-data' ],
        filemtime( get_template_directory() . '/js/editor-sidebar.js' ),
        true
    );
}

add_action( 'init', 'register_reading_notes_meta' );

function register_reading_notes_meta() {
    register_post_meta( 'post', '_reading_notes', [
        'show_in_rest'  => true,
        'single'        => true,
        'type'          => 'string',
        'auth_callback' => function () {
            return current_user_can( 'edit_posts' );
        },
    ] );
}

Step 2 — the sidebar JavaScript (js/editor-sidebar.js). Uses withSelect / withDispatch to connect the textarea to the post meta store:

( function () {
    var el                        = wp.element.createElement;
    var Fragment                  = wp.element.Fragment;
    var PluginSidebar             = wp.editPost.PluginSidebar;
    var PluginSidebarMoreMenuItem = wp.editPost.PluginSidebarMoreMenuItem;
    var registerPlugin            = wp.plugins.registerPlugin;
    var TextareaControl           = wp.components.TextareaControl;
    var withSelect                = wp.data.withSelect;
    var withDispatch              = wp.data.withDispatch;

    var SidebarContent = withSelect( function ( select ) {
        return { meta: select( 'core/editor' ).getEditedPostAttribute( 'meta' ) };
    } )( withDispatch( function ( dispatch ) {
        return {
            updateMeta: function ( value ) {
                dispatch( 'core/editor' ).editPost( { meta: { _reading_notes: value } } );
            },
        };
    } )( function ( props ) {
        return el( TextareaControl, {
            label:    'Reading Notes',
            value:    props.meta._reading_notes || '',
            onChange: props.updateMeta,
            rows:     6,
        } );
    } ) );

    registerPlugin( 'my-custom-sidebar', {
        render: function () {
            return el(
                Fragment, null,
                el( PluginSidebarMoreMenuItem, { target: 'my-custom-sidebar' }, 'Reading Notes' ),
                el( PluginSidebar, { name: 'my-custom-sidebar', title: 'Reading Notes' },
                    el( SidebarContent, null )
                )
            );
        },
    } );
} )();

NOTE: The withSelect / withDispatch higher-order components shown above were the standard pattern in WordPress 5.x (late 2019). In modern projects you will typically see the equivalent useSelect and useDispatch React hooks — they are functionally identical but require a JSX build step. The plain createElement approach used here runs without any build tooling and is a good starting point before adding a webpack or @wordpress/scripts build pipeline.