Gutenberg’s SlotFill system is a publish/subscribe mechanism for injecting React components into predefined positions (Slots) in the editor UI — without modifying core editor components. Slots are named insertion points exposed by WordPress or plugins; Fills are components that render into those slots. You can think of it as the JavaScript equivalent of WordPress action hooks, but for UI components rather than PHP callbacks. WordPress exposes several named slots in the editor: PluginDocumentSettingPanel (document sidebar tab), PluginSidebar (full custom sidebar), PluginMoreMenuItem (the “⋮” overflow menu in the toolbar), MainDashboardButton, and BlockControls (already covered elsewhere). Using SlotFill lets plugins extend the editor UI in a stable, upgrade-safe way rather than DOM manipulation or overriding React components.
Problem: A plugin needs to add a "Share to Social" menu item in the post editor's toolbar "More options" menu (the three-dot menu), and a "Reading Time" panel in the document settings sidebar. Both must be added without modifying any WordPress core files.
Solution: Use PluginMoreMenuItem to add the toolbar menu item and PluginDocumentSettingPanel for the sidebar panel — both from @wordpress/edit-post. Register via registerPlugin().
// plugin.js — loaded via enqueue_block_editor_assets
const { registerPlugin } = wp.plugins;
const { PluginMoreMenuItem, PluginDocumentSettingPanel } = wp.editPost;
const { useSelect } = wp.data;
const { __ } = wp.i18n;
// ── Plugin with both MoreMenuItem and DocumentSettingPanel ────────────
registerPlugin( 'my-social-plugin', {
render: function MyPlugin() {
// Read current post content to calculate reading time
const content = useSelect( ( select ) =>
select( 'core/editor' ).getEditedPostAttribute( 'content' ) ?? '',
[] );
const wordCount = content.trim().split( /\s+/ ).length;
const readingTime = Math.max( 1, Math.ceil( wordCount / 200 ) );
return (
<>
{ /* ─── Adds item to the ⋮ toolbar overflow menu ─────── */ }
<PluginMoreMenuItem
icon="share"
onClick={ () => {
const url = wp.data.select( 'core/editor' ).getPermalink();
const title = wp.data.select( 'core/editor' ).getEditedPostAttribute( 'title' );
window.open(
`https://twitter.com/intent/tweet?url=${ encodeURIComponent( url ) }&text=${ encodeURIComponent( title ) }`,
'_blank'
);
} }
>
{ __( 'Share to Twitter', 'textdomain' ) }
</PluginMoreMenuItem>
{ /* ─── Adds a panel to the Document sidebar ──────────── */ }
<PluginDocumentSettingPanel
name="reading-time-panel"
title={ __( 'Reading Time', 'textdomain' ) }
icon="clock"
>
<p>
{ `${ wordCount } words — approx. ${ readingTime } min read` }
</p>
</PluginDocumentSettingPanel>
</>
);
},
} );
<?php
add_action( 'enqueue_block_editor_assets', function () {
wp_enqueue_script(
'my-social-plugin',
plugin_dir_url( __FILE__ ) . 'build/plugin.js',
[ 'wp-plugins', 'wp-edit-post', 'wp-element', 'wp-data', 'wp-i18n' ],
'1.0.0',
true
);
} );
NOTE: registerPlugin() takes a name (must be unique, use your plugin prefix) and a render function that can return multiple SlotFill components. All SlotFills registered in the same registerPlugin() call are co-located as siblings, which is a common source of confusion — they do not render next to each other in the UI, they each render into their respective named Slot wherever that Slot is in the editor DOM. The icon prop accepts a dashicon slug string, a Dashicon component, or an SVG element. PluginDocumentSettingPanel renders in the "Document" tab of the post sidebar (the same tab that contains the "Status & Visibility" and "Permalink" panels) — not in a separate tab. For a full custom sidebar tab, use PluginSidebar instead.