Honor your inner monk: simpler adminhtml UIs for complex Magento 2 work

The Magento 2 admin panel grows by accumulation. Every module adds a menu entry under its own vendor node, every config lands in its own section, and after a few years the navigation looks like a franchise directory rather than a tool. This article is for Magento 2 backend developers who want to stop signing their name on the Magento 2 admin menu and start putting functionality where operators already look for it.

The takeaway: build your Magento 2 admin menu around the platform’s existing taxonomy, not your vendor name. Declare your menu.xml nodes and system.xml sections under the core groups — Catalog, Sales, Marketing, System — where operators already look.


Why this matters now

The pattern is consistent across codebases: a module ships, a top-level menu entry appears under Vendor Name → Feature, a config section gets added under Stores → Configuration → Vendor Name, and a support ticket arrives three weeks later asking where the setting is. Multiply that by ten modules and the admin becomes a vendor catalogue, not a workspace. Nobody removes entries — they only add.


The baseline

A reasonable Magento 2 module today registers its own menu.xml node with a vendor-named parent and adds a system.xml section under a vendor-named tab. The result is discoverable for the developer who wrote it and nobody else. Operators who use the admin daily think in terms of Catalog, Sales, Marketing, Content — the built-in taxonomy Magento 2 ships. Anything outside that taxonomy creates a parallel navigation layer that competes for attention without earning it.

That’s not malicious — it’s the path of least resistance when scaffolding a module. The cost is a navigation that rewards module authors, not operators.


The trade-off

Place Magento 2 admin menu entries where the function belongs, not where the vendor lives

Magento 2’s menu.xml lets any module declare a child of any existing node — including Magento’s own top-level nodes. An import management tool belongs under System, not under AcmeCorp. A product enrichment queue belongs under Catalog. A loyalty programme grid belongs under Marketing. The operator already knows where to look; the only question is whether you meet them there.

Declare the parent as an existing core node and set sortOrder to slot the entry into a logical position within that group. An AMQP monitoring tool belongs under System → Tools alongside Import and Export — not under a vendor node:

<!-- etc/adminhtml/menu.xml -->
<add id="Brocode_AmqpMonitor::amqp_monitor"
     title="AMQP Monitor"
     module="Brocode_AmqpMonitor"
     sortOrder="25"
     parent="Magento_Backend::system_tools"
     action="brocode_amqpmonitor/monitor/index"
     resource="Brocode_AmqpMonitor::amqp_monitor"/>

If no existing parent fits precisely, the second-best option is a child of the closest core node rather than a new top-level vendor node. A new top-level node is justified only when the module introduces a genuinely new domain — a POS system, a B2B portal — that has no core analogue. An import tool does not meet that bar.

Strengths

  • Operators find the feature through existing muscle memory. No training needed.
  • The admin menu stays flat; every new entry is a refinement of existing structure rather than an expansion of it.

Costs

  • The feature is less visible as “your module’s work” — which is the point, and occasionally a client relations problem. Name the menu entry after the function, add a comment in menu.xml attributing the module.
  • You need to know the core node IDs. They’re in Magento_Backend/etc/adminhtml/menu.xml and each module’s own menu.xml; grep for the id attribute.

Attach config to existing tabs and sections instead of creating new ones

system.xml has three levels: tabsectiongroupfield. Most modules create a new tab and a new section. The tab is the top-level bucket in the left sidebar of Stores → Configuration; adding one per vendor produces a sidebar that scrolls past useful content to reach vendor-branded noise.

The fix is to declare <tab> only when genuinely warranted, and otherwise attach <section> to a core tab — or attach <group> directly to an existing core section. Magento 2’s built-in tabs (general, catalog, sales, advanced) cover the vast majority of real configuration domains.

Attach a new section to an existing tab:

<!-- etc/adminhtml/system.xml -->
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
    <system>
        <!-- No <tab> declaration — reuse the core "services" tab -->
        <section id="brocode_amqpmonitor"
                 translate="label"
                 sortOrder="150"
                 showInDefault="1"
                 showInWebsite="0"
                 showInStore="0">
            <tab>services</tab>
            <label>Amqp Monitor</label>
            <resource>Brocode_AmqpMonitor::config</resource>
            <group id="general" translate="label" sortOrder="10" showInDefault="1" showInWebsite="0" showInStore="0">
                <label>General</label>
                <field id="enabled" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="0" showInStore="0">
                    <label>Enable AMQP Monitor</label>
                    <!-- Brocode_AmqpMonitor: controls queue health monitoring -->
                    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                </field>
            </group>
        </section>
    </system>
</config>

Go further: attach a group directly to an existing core section:

When a module only needs one or two config fields and they are a natural extension of an existing Magento 2 section, skip the new section entirely and add a group to the core one. Catalog-related image processing config belongs in catalog/product_image, not in a new vendor/image_processing section:

<!-- etc/adminhtml/system.xml -->
<system>
    <section id="catalog">
        <group id="acme_image_processing"
               translate="label"
               sortOrder="200"
               showInDefault="1"
               showInWebsite="0"
               showInStore="0">
            <label>Image Processing</label>
            <field id="resize_on_import" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="0" showInStore="0">
                <label>Resize images on import</label>
                <!-- Acme_ImageProcessing: controls automatic resizing during product import -->
                <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
            </field>
        </group>
    </section>
</system>

The <section id="catalog"> reference here does not redeclare the section — it extends it. Magento 2 merges system.xml across all modules; you’re appending a group to a section that already exists.

Strengths

  • Config is findable without knowing which vendor module added it — operators look in Catalog for catalog-related settings because that’s where they’ve always been.
  • Fewer sidebar entries means less scrolling and less visual noise in a screen operators visit often.

Costs

  • Fields buried in a core section can be harder to find in documentation specific to your module. Compensate with clear <comment> text on each field naming the module responsible.
  • sortOrder requires coordination if multiple modules extend the same section. Use a number in the 150–250 range for third-party additions to avoid collisions with core groups (which tend to use 10–100).

A working example: the cleanup module

The fastest way to apply this to an existing vendor module is a dedicated cleanup module — a thin layer that declares a dependency on the vendor module and overrides only navigation and config placement. No patches, no forks, survives composer updates. For the broader module-structure rationale this builds on, see Magento 2 module architecture in practice.

Module declaration with explicit dependency:

The <sequence> entry ensures the cleanup module loads after the vendor module, so its menu.xml and system.xml merges win.

<!-- etc/module.xml -->
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="Acme_AdminCleanup">
        <sequence>
            <!-- Must load after every module whose navigation this cleanup touches -->
            <module name="Acme_Shipping"/>
            <module name="Acme_Loyalty"/>
        </sequence>
    </module>
</config>

Removing or moving a vendor menu entry:

Three operations cover every cleanup case. <remove> wipes a node entirely. <update> changes attributes on an existing node in place — useful for correcting a parent or sortOrder without touching the entry’s id or action. <add> re-registers a node from scratch, typically after a <remove>.

<!-- etc/adminhtml/menu.xml -->
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Backend:etc/menu.xsd">
    <menu>
        <!-- Option A: move an entry to a different parent in place.
             Use <update> when the node id and action stay the same — only
             the parent or sortOrder is wrong. -->
        <update id="Acme_Loyalty::loyalty_dashboard"
                parent="Magento_Backend::marketing"
                sortOrder="75"/>

        <!-- Option B: remove a vendor top-level node entirely... -->
        <remove id="Acme_Shipping::shipping_root"/>

        <!-- ...then re-add the one useful child under the correct core parent.
             Use <remove> + <add> when parent, action, or title all need to change. -->
        <add id="Acme_Shipping::shipping_rates"
             title="Shipping Rates"
             module="Acme_Shipping"
             sortOrder="60"
             parent="Magento_Sales::sales"
             action="acme_shipping/rates/index"
             resource="Acme_Shipping::shipping_rates"/>
    </menu>
</config>

Grep vendor/acme/module-shipping/etc/adminhtml/menu.xml for the exact id values before writing any of these. A wrong ID fails silently — the node stays or the update is ignored.

Moving a config section to the correct domain tab:

Overriding <tab> and <sortOrder> in the cleanup module’s system.xml repositions the section without touching the vendor file. Magento 2 merges system.xml by section ID; the last-loaded value wins — which is the cleanup module, thanks to <sequence>.

<!-- etc/adminhtml/system.xml -->
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
    <system>
        <!-- Acme_Shipping originally placed this under their own "acme" tab.
             Shipping belongs under "sales". Repoint the tab and fix sort order. -->
        <section id="acme_shipping" sortOrder="160">
            <tab>sales</tab>
        </section>

        <!-- Acme_Loyalty put rewards config under "acme" tab too.
             Marketing is the correct domain. -->
        <section id="acme_loyalty" sortOrder="170">
            <tab>marketing</tab>
        </section>

        <!-- Acme_Payment landed in the correct "payment" tab but its sortOrder
             pushes it above Magento's own payment methods. Fix the position only. -->
        <section id="acme_payment" sortOrder="155"/>
    </system>
</config>

Only declare the attributes that need changing. If the cleanup module redeclares a full <section> with all its attributes copied from the vendor, any change the vendor ships in an update — a new showInWebsite flag, a renamed resource, a corrected label — is silently overridden by the cleanup module and never reaches the running store. The self-closing <section id="acme_payment" sortOrder="155"/> above is the right mental model: touch one thing, leave everything else to the vendor.

Check the vendor module first — some ship a disable flag:

Before writing any override code, check the vendor’s system.xml for a field like enabled, active, or show_in_menu. Some vendors expose a config flag that hides their menu entry without any custom module needed. Magefan modules, for example, ship a Display Magefan Menu Item toggle (mfextension/menu/display) directly in Stores → Configuration — setting it to No removes the menu entry after a cache flush, no code required. That flag is always the preferable path — it is the vendor’s own supported mechanism and will not break on updates. The cleanup module approach is for when no such flag exists.

Magefan toggle that removes a vendor entry from the Magento 2 admin menu via Stores → Configuration, no custom code required.
Magefan’s built-in menu visibility toggle in Stores → Configuration — set to No to remove the admin menu entry without any custom code.

What to skip

  • A top-level menu node for a single feature. If the module has one grid and one config section, it does not need its own top-level node. Find the closest core parent and use it.
  • A vendor-named tab in Stores → Configuration. Tab proliferation is the config equivalent of a cluttered menu. If there is no genuine reason an operator would think “I need to look under AcmeCorp“, the tab should not exist.
  • sortOrder="10" for third-party groups. That range belongs to core. Use 150+ to avoid collisions with Magento 2 updates.

Verification

Your Magento 2 admin menu is clean when:

  • A new operator can find the module’s primary grid without being told which menu section it lives under.
  • Stores → Configuration sidebar has no vendor-named tab that requires mental translation to a function.
  • bin/magento setup:upgrade produces no duplicate section or menu ID warnings in var/log/system.log.
  • Every config field carries a <comment> naming the module — compensation for the deliberate absence of a vendor-branded container.

Related modules

  • If you want a worked example of an operator-first admin tool that already follows this Magento 2 admin menu placement discipline, see the Magento 2 Store Overview module.

Related reading


Related Topics


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *