Mantle's admin interface is a React application with two extension points: a filter that declares where something appears, and a SlotFill that supplies what appears there. You never need to modify Mantle's own components.
The two-part pattern
Every bundled module follows the same shape. The filter creates the navigation entry; registerPlugin() plus a fill renders the content.
How a tab reaches the screen
Add a top-level tab
-
Create the view component
A plain component that returns content. It does not need a
Cardwrapper —ViewWrappersupplies one. -
Register the tab
Add a tab object through the
mantle.navigation_tabsfilter withid,path,title,component,order, andrequiredCapability. -
Register the panel fill
Call
registerPlugin()with aMantlePanelsfill whosetabIdmatches the tabid. -
Navigate
Visit
admin.php?page=mantle&view=<id>.
import { addFilter } from '@wordpress/hooks';import { registerPlugin } from '@wordpress/plugins';import { __ } from '@wordpress/i18n';import Reports from './index';import MenuIcon from './MenuIcon';import { MantlePanels } from '@slots';const slug = 'reports';addFilter( 'mantle.navigation_tabs', `mantle/${ slug }`, ( tabs ) => [ ...tabs, { id: slug, path: `/${ slug }`, title: __( 'Reports', 'mantle' ), icon: MenuIcon, order: 60, component: Reports, requiredCapability: 'mantle_view_reports', },] );registerPlugin( `mantle-${ slug }`, { render: () => ( <MantlePanels tabId={ slug }> <Reports /> </MantlePanels> ),} );
import { __ } from '@wordpress/i18n';/** * Pure content. ViewWrapper wraps this in a Card with the tab title * as its heading, so do not add your own heading or Card. */const Reports = () => ( <p>{ __( 'Nothing to report yet.', 'mantle' ) }</p>);export default Reports;
Tab object properties
Tab object
getTabs() and ViewWrapper. Shapes verified against src/constants/routes.js and the bundled register.js files.-
idstring required -
Unique kebab-case identifier. Becomes the
?view=query value. -
pathstring required -
Leading-slash path form of the ID, used by the path and location helpers.
-
titlestring required -
Shown in navigation and rendered as the Card heading by ViewWrapper.
-
componentany required -
The React component to render.
-
ordernumber -
Navigation sort order. Dashboard is 10 and Settings is 100, so module tabs sit between. Use multiples of ten.
-
iconany -
Component rendered as the navigation icon.
-
requiredCapabilitystring -
Capability needed to see the tab.
-
moduleIdstring -
The PHP module ID, when it differs from the tab slug. Lets the sidebar resolve the module’s enabled state.
No fields match this filter.
Add a Settings subtab
Use mantle.settings_tabs with a Mantle.SettingsTabs fill. Naming the subtab after a module ID gives you automatic visibility gating — the Settings view removes subtabs whose name matches an inactive module's slug.
addFilter( 'mantle.settings_tabs', 'mantle/reports/register', ( tabs ) => [ ...tabs, { name: 'reports', title: __( 'Reports', 'mantle' ), priority: 50, className: 'mantle-settings-tab-reports', },] );registerPlugin( 'mantle-settings-reports', { scope: 'mantle-settings-tabs', render: () => ( <MantleSettingsTabs settingsTabId="reports" area="content"> <ReportsSettings /> </MantleSettingsTabs> ),} );
-
Line 12Omitting
scopeis the most common reason a subtab renders blank. -
Line 14Must equal the
namedeclared in the filter.
Add a dashboard widget
import { registerPlugin } from '@wordpress/plugins';import { __ } from '@wordpress/i18n';import { MantleDashboardWidgets } from '@slots';const ReportsWidget = () => ( <div> <h3>{ __( 'Reports', 'mantle' ) }</h3> <p>{ __( 'Nothing to report yet.', 'mantle' ) }</p> </div>);registerPlugin( 'mantle-reports-widget', { render: () => ( <MantleDashboardWidgets> <ReportsWidget /> </MantleDashboardWidgets> ),} );
Where the pieces live
-
constants/-
routes.js
-
-
components/-
ViewWrapper.js
-
-
layouts/-
index.js
-
-
slots/-
index.js -
panels.js -
settings-tabs.js -
dashboard-widgets.js
-
-
modules/-
Security/-
register.js -
index.js -
MenuIcon.js
-
-
Modules/-
register.js
-
-
-
views/-
Dashboard/ -
Setup/
-
Build and verify
npm run buildThen load admin.php?page=mantle&view=reports. If the tab is missing, work through these in order:
Tab not appearing
-
Filter returned an objectstring -
The callback must return an array. This is the most common cause.
-
register.js never importedstring -
The module’s register.js has to be reachable from the admin entry point, or the filter is never added.
-
Capability missingstring -
requiredCapabilitynames a capability the current user lacks. Register it and run a capability sync. -
Bundle not rebuiltstring -
Run
npm run build, ornpm run startwhile developing. -
tabId mismatchstring -
The tab appears but the panel is empty: the
tabIdon MantlePanels does not match the tabid.
No fields match this filter.
See also
- SlotFills reference — every available slot.
- Hooks reference — the filters in full.
- Admin UI architecture — how routing works.