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 |

Prerequisites
Before writing any code, make sure you have:
- A working WordPress 6.5+ installation (this guide is tested up to WordPress 6.8).
- A custom plugin folder, e.g.
wp-content/plugins/pp-custom-sidebar/. - Node.js 20+ and @wordpress/scripts installed for building your JS.
- 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.

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.

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(). |

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.
