How to Add Custom Sidebar Panels to the Gutenberg Editor with SlotFill

Robert Halley

Extending the WordPress block editor is one of the most powerful things you can do as a developer, and adding a Gutenberg custom sidebar panel is often the cleanest way to surface plugin settings, post meta, or workflow controls right where editors are working. In this practical tutorial, we’ll walk through exactly how to register a sidebar panel using SlotFill and the @wordpress/plugins API, with a complete working example you can drop into your own plugin today.

What Is SlotFill and Why Use It for Custom Sidebar Panels?

SlotFill is a React pattern baked into Gutenberg that lets your code inject UI into predefined locations of the editor without hacking core. Think of a Slot as an empty placeholder defined by WordPress, and a Fill as the content your plugin pushes into it.

For sidebar panels, the two main components you’ll use are:

  • PluginSidebar: registers a brand new sidebar (with its own icon next to the gear icon).
  • PluginDocumentSettingPanel: adds a panel inside the existing Document sidebar (the default settings panel).

Both are registered through registerPlugin() from @wordpress/plugins.

When to Use Which?

Use Case Recommended Component
Plugin-specific tools, large settings UI PluginSidebar
A few extra fields tied to the post itself PluginDocumentSettingPanel
SEO, scheduling, custom taxonomies PluginDocumentSettingPanel
External integrations, dashboards PluginSidebar
wordpress code editor screen

Prerequisites

Before writing any code, make sure you have:

  1. A working WordPress 6.5+ installation (this guide is tested up to WordPress 6.8).
  2. A custom plugin folder, e.g. wp-content/plugins/pp-custom-sidebar/.
  3. Node.js 20+ and @wordpress/scripts installed for building your JS.
  4. Basic familiarity with React and ES modules.

Step 1: Bootstrap the Plugin

Create the main PHP file at pp-custom-sidebar/pp-custom-sidebar.php:

<?php
/**
 * Plugin Name: PP Custom Sidebar
 * Description: Adds a Gutenberg custom sidebar panel using SlotFill.
 * Version: 1.0.0
 * Author: Pixel Perfect Portfolios
 */

if ( ! defined( 'ABSPATH' ) ) { exit; }

add_action( 'enqueue_block_editor_assets', function () {
    $asset = include plugin_dir_path( __FILE__ ) . 'build/index.asset.php';

    wp_enqueue_script(
        'pp-custom-sidebar',
        plugins_url( 'build/index.js', __FILE__ ),
        $asset['dependencies'],
        $asset['version'],
        true
    );
} );

// Register the post meta so REST + the editor can read/write it.
add_action( 'init', function () {
    register_post_meta( 'post', '_pp_subtitle', [
        'show_in_rest'  => true,
        'single'        => true,
        'type'          => 'string',
        'auth_callback' => function () {
            return current_user_can( 'edit_posts' );
        },
    ] );

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

The show_in_rest flag is critical. Without it, the block editor cannot read or save your meta values.

wordpress code editor screen

Step 2: Set Up package.json

{
  "name": "pp-custom-sidebar",
  "version": "1.0.0",
  "scripts": {
    "build": "wp-scripts build",
    "start": "wp-scripts start"
  },
  "devDependencies": {
    "@wordpress/scripts": "^30.0.0"
  }
}

Run npm install then npm run start to compile on the fly.

Step 3: Build the Custom Sidebar Panel

Create src/index.js. This example registers both a dedicated sidebar (with its own icon) and a panel inside the document sidebar, so you can see both patterns at once.

import { registerPlugin } from '@wordpress/plugins';
import {
    PluginSidebar,
    PluginSidebarMoreMenuItem,
    PluginDocumentSettingPanel,
} from '@wordpress/editor';
import { PanelBody, TextControl, ToggleControl } from '@wordpress/components';
import { useSelect, useDispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { starFilled } from '@wordpress/icons';

const PP_PLUGIN_NAME = 'pp-custom-sidebar';

const MetaFields = () => {
    const { subtitle, featured } = useSelect( ( select ) => {
        const meta = select( 'core/editor' ).getEditedPostAttribute( 'meta' ) || {};
        return {
            subtitle: meta._pp_subtitle || '',
            featured: !! meta._pp_featured_flag,
        };
    }, [] );

    const { editPost } = useDispatch( 'core/editor' );

    return (
        <>
            <TextControl
                label={ __( 'Subtitle', 'pp' ) }
                value={ subtitle }
                onChange={ ( value ) =>
                    editPost( { meta: { _pp_subtitle: value } } )
                }
            />
            <ToggleControl
                label={ __( 'Mark as featured', 'pp' ) }
                checked={ featured }
                onChange={ ( value ) =>
                    editPost( { meta: { _pp_featured_flag: value } } )
                }
            />
        </>
    );
};

const PPSidebar = () => (
    <>
        <PluginSidebarMoreMenuItem target="pp-sidebar" icon={ starFilled }>
            { __( 'Pixel Perfect Settings', 'pp' ) }
        </PluginSidebarMoreMenuItem>

        <PluginSidebar
            name="pp-sidebar"
            title={ __( 'Pixel Perfect Settings', 'pp' ) }
            icon={ starFilled }
        >
            <PanelBody title={ __( 'Post Meta', 'pp' ) } initialOpen={ true }>
                <MetaFields />
            </PanelBody>
        </PluginSidebar>

        <PluginDocumentSettingPanel
            name="pp-doc-panel"
            title={ __( 'Pixel Perfect', 'pp' ) }
            className="pp-doc-panel"
        >
            <MetaFields />
        </PluginDocumentSettingPanel>
    </>
);

registerPlugin( PP_PLUGIN_NAME, {
    render: PPSidebar,
    icon: starFilled,
} );

Run npm run build, activate the plugin, and open any post in the editor. You should see:

  • A new star icon in the top right of the editor that opens your dedicated sidebar.
  • A new Pixel Perfect panel inside the Document settings sidebar.
  • Both panels share the same fields and stay in sync because they read from the same post meta.
wordpress code editor screen

How the Code Works

registerPlugin()

This is the entry point of the @wordpress/plugins API. It tells Gutenberg “here is a render function, mount it into the editor.” Whatever you return becomes a Fill that gets placed into the matching Slot.

useSelect and useDispatch

These hooks from @wordpress/data are how we read and write post meta reactively. getEditedPostAttribute('meta') always reflects unsaved edits, while editPost() queues the change so it gets included with the next post save.

Why Both Sidebars?

Showing both a PluginSidebar and a PluginDocumentSettingPanel is a common UX choice: power users get a discoverable dedicated panel, while casual editors find the same controls in the familiar Document sidebar.

Common Pitfalls (and Fixes)

Issue Fix
Panel does not appear at all Confirm the script is enqueued via enqueue_block_editor_assets, not wp_enqueue_scripts.
Meta values do not save Make sure show_in_rest is true and auth_callback returns true for the user.
Imports throw errors In WordPress 6.6+, import sidebar components from @wordpress/editor rather than @wordpress/edit-post.
Panel shows for wrong post types Wrap the render with a check on getCurrentPostType().
wordpress code editor screen

Restricting the Panel to Specific Post Types

Need the sidebar only on a custom post type like portfolio? Add a guard:

const PPSidebar = () => {
    const postType = useSelect(
        ( select ) => select( 'core/editor' ).getCurrentPostType(),
        []
    );

    if ( postType !== 'portfolio' ) {
        return null;
    }

    // ...rest of the render
};

Going Further

Once you are comfortable with the basics, here are some patterns worth exploring:

  • Use PluginPrePublishPanel and PluginPostPublishPanel to add checks before/after publishing.
  • Combine your sidebar with the @wordpress/api-fetch package to call custom REST endpoints.
  • Use the @wordpress/components library (ColorPicker, ComboboxControl, DatePicker) for richer UI.
  • Persist UI state with @wordpress/preferences so user choices survive page reloads.

FAQ

What is the difference between PluginSidebar and PluginDocumentSettingPanel?

PluginSidebar creates a brand new sidebar accessible from its own icon in the editor toolbar. PluginDocumentSettingPanel adds a collapsible section inside the existing Document sidebar. Use the first for self-contained tools, the second for fields that belong to the post itself.

Do I need React knowledge to add a Gutenberg custom sidebar panel?

Yes, at least the basics. The block editor is a React application, and SlotFill components are React components. You do not need advanced patterns like reducers or context providers, but JSX and hooks (useState, useSelect) are required reading.

Can I add a custom sidebar without using JavaScript build tools?

Technically yes, by writing pre-bundled JS that imports from the global wp object. In practice, @wordpress/scripts is the supported and recommended path. It gives you sourcemaps, JSX support, and proper dependency tracking out of the box.

Why is my custom panel not showing up?

The most common reasons are: the script is enqueued on the wrong hook, the plugin name passed to registerPlugin() contains invalid characters (it must be lowercase with dashes only), or imports are coming from a deprecated package. Check the browser console for warnings.

Does this approach work with the Site Editor (FSE)?

Yes. Since WordPress 6.6, sidebar SlotFill components imported from @wordpress/editor work in both the post editor and the site editor with the same code.

How do I save data that is not post meta?

Create a custom REST endpoint with register_rest_route and call it from the sidebar using apiFetch. This is the right pattern for plugin-wide settings or external API integrations that do not belong on a single post.

Wrapping Up

Adding a Gutenberg custom sidebar panel with SlotFill takes less than 100 lines of code once you understand the pieces: registerPlugin() as the entry point, PluginSidebar or PluginDocumentSettingPanel as the slot, and useSelect / useDispatch to wire up data. From there, the editor becomes a canvas you can extend without ever touching core.

Need help building production-grade editor extensions for your WordPress project? Get in touch with our team, we ship Gutenberg integrations every week.

Leave a Comment