Groups WordPress admin menu items into named, collapsible categories inside the native sidebar. Sorts what it recognises automatically, and lets you rearrange everything else by drag and drop.
License: GPLv2 or later · Requires WordPress 6.4+ · Requires PHP 7.4+
Not yet on the WordPress.org directory, so install it from a zip for now.
- Download the latest
admin-menu-organizer.<version>.zipfrom Releases. - In your dashboard go to Plugins → Add New → Upload Plugin.
- Choose the zip, click Install Now, then Activate.
- Your sidebar is grouped immediately, using the automatic rules. Nothing to configure.
- To rearrange anything, go to Settings → Menu Organizer.
Requires PHP 7.4+ and Composer.
git clone https://github.com/submoro/admin-menu-organizer.git
cd admin-menu-organizer
composer install
composer run buildThat runs the full gate — linting, tests, security audit, readme validation —
then writes build/admin-menu-organizer.<version>.zip. Upload that as above.
If you have filesystem access, clone straight into your plugins directory. The
repository folder name must be admin-menu-organizer, because WordPress derives
the text domain from it:
cd wp-content/plugins
git clone https://github.com/submoro/admin-menu-organizer.gitThen activate it from the Plugins screen. No composer install is needed to
run the plugin — it has no runtime dependencies. Composer is only for the
development tooling.
wp plugin install /path/to/admin-menu-organizer.1.0.0.zip --activate| WordPress | 6.4 or newer |
| PHP | 7.4 or newer |
| Multisite | Works per-site. The network admin menu is deliberately left untouched. |
Deactivating restores the stock sidebar exactly and keeps your saved arrangement, so reactivating brings it back. Deleting the plugin removes every option and user meta key it created — there is no residue.
A production site accumulates thirty to sixty plugins, and every one of them adds
a top-level item to wp-admin's sidebar. The result is a flat, unsorted list
several screens long, with an SEO plugin next to a mail-delivery plugin next to a
firewall. There is no grouping and no way to impose order without editing code.
It reorders and decorates the existing menu. It never rebuilds it, never removes an item, and never touches a capability.
- Groups top-level items into categories: Content, Commerce, Design, SEO, Security, Performance, Users, Integrations, Tools.
- Recognises plugins on sight and files them correctly — including plugins it has never heard of, see Detection.
- Full drag-and-drop override, with a keyboard equivalent for every drag.
- Per-user open/closed memory; the group containing the current page always opens.
- Collapsed groups show an aggregated update-count badge.
- Site-wide default, optional per-role layouts, optional per-user personalisation.
- Never hides an item. Anything unrecognised goes to an always-visible Other group. Nothing becomes unreachable in any configuration.
- Never modifies index
1of a menu item, so it cannot grant or revoke access. - Does not restyle the admin beyond the menu.
- Does not touch the network admin menu on multisite; that screen is left alone.
- Makes zero outbound HTTP requests. No telemetry, no analytics, no calling home. This is enforced by the test suite, not merely promised.
The interesting part. Rather than a lookup table that goes stale, detection is an eight-layer cascade, each layer more general than the last:
| Layer | Signal |
|---|---|
| 1 | Explicit human override in the saved layout |
| 2 | Core slug map (edit.php, themes.php, …) |
| 3 | Curated table of 343 known plugin menu slugs |
| 4 | Post-type defaulting — edit.php?post_type=X, with unknown types → Content |
| 5 | Vendor prefixes (wpseo, wc-, elementor, yith_, tribe_, …) |
| 6 | Namespaced capabilities (manage_woocommerce, wpseo_manage_options, …) |
| 7 | Weighted keyword scoring over title and slug, with a confidence floor |
| 8 | Dashicon as a tiebreak (dashicons-cart, dashicons-shield, …) |
Measured against 40 real plugins deliberately excluded from the table: 40 / 40 correct. The highest-value rule is layer 4 — an unrecognised custom post type is content, which files every CPT on every site without naming any.
When signals tie or score too low, it declines rather than guesses, and the item lands in the always-visible Other group.
Extend it without forking:
add_filter( 'amorg_known_slugs', function ( $map ) { $map['my-plugin'] = 'commerce'; return $map; } );
add_filter( 'amorg_category_definitions', function ( $groups ) { /* add your own */ return $groups; } );
add_filter( 'amorg_resolve_group', function ( $group_id, $item ) { return $group_id; }, 10, 2 );
add_filter( 'amorg_resolved_layout', function ( $layout ) { return $layout; } );Two escape hatches, both of which always work and neither of which discards your saved arrangement:
?amorg=off # disables it for one page load
define( 'AMORG_DISABLE', true ); # in wp-config.php, disables it entirely
WordPress builds $GLOBALS['menu'] during admin_menu and renders it in
wp-admin/menu-header.php. Every capability check, update bubble, current
highlight and third-party integration depends on that pipeline staying intact, so
this plugin leaves it intact.
| Hook | Purpose |
|---|---|
custom_menu_order |
Unlock reordering |
menu_order |
Return every slug, deduped, grouped contiguously |
add_menu_classes |
Decorate items; inject group header rows |
admin_body_class |
Emit collapsed state, server-side |
Ordering, grouping and collapsed state are all server-rendered, so there is no flash of an ungrouped menu. JavaScript only fills in the header's button, into a row whose height CSS already reserves — so no layout shift. With JavaScript off, every group renders expanded and fully usable.
Design notes, and the four places WordPress core contradicted the original
specification, are in docs/core-notes.md and
docs/decisions.md.
composer install
composer run test # unit suite, needs no database
composer run lint # phpcs: WordPress, -Extra, -Docs, PHPCompatibilityWP
composer run audit # security audit against the project's own rules
composer run build # full gate, then produce the release zipThe test suite is split. tests/unit/ needs nothing but PHP and runs anywhere;
tests/integration/ needs WordPress and a database and runs in CI. That split
forces the detector, sanitiser, reorderer and migration chain to be pure
functions of their arguments, which is better structure regardless.
A caveat worth knowing:
phpcsdoes not prove PHP 7.4 compatibility here. The current stable PHPCompatibility release predates PHP 8.0 and cannot see PHP 8 syntax — verified with a canary using?->andmatchthat it waved straight through. The binding check is thesyntax-php74CI job, which parses every shipped file with a real PHP 7.4 binary.
CI runs 11 jobs, all green:
| Plugin Check | passes all five categories, run against the built zip rather than the repo |
| Integration suite | 29 tests on PHP 7.4 / 8.1 / 8.3 against both WordPress 6.4 and current stable |
| Unit suite | 295 tests, 2995 assertions, on PHP 7.4 / 8.1 / 8.3 |
| PHP 7.4 syntax | every shipped file parsed by a real 7.4 binary |
| Coding standards | WordPress, -Extra, -Docs, PHPCompatibilityWP |
| No outbound requests | source scan, plus a pre_http_request tripwire in the suite |
The $menu internals, the menu_order semantics, the finished accordion render
and the compiled .mo are each proven by executing real WordPress core code
rather than a reimplementation of it — see docs/recon/.
What is not verified: anything requiring a rendered page. The nine colour
schemes, RTL, the responsive breakpoints, keyboard navigation and a screen-reader
pass have not been looked at in a browser. Honest status in
docs/decisions.md.
See docs/release.md for the pre-submission gate, the zip
build and the WordPress.org SVN layout.
GPLv2 or later. See LICENSE.
Icons and banners are original artwork generated by
bin/build-assets.php. No third-party assets, fonts or
libraries are bundled; the drag-and-drop editor uses jQuery UI Sortable, which
ships with WordPress.