Problem/Motivation
On sites with thousands or sometimes only hundreds of menu items in the same menu, the UI runs out of memory and displays either a WSOD or a memory error depending on the environment settings.
The root cause is that menu_ui's edit form loads the entire menu tree and builds a single tabledrag form element for every link on every page load, so its cost scales linearly with link count. For typical menus (a few dozen links) this is fine, and there is no reason to change it. On large menus the form becomes expensive to build and submit, and depending on environment settings the page can fail outright with a WSOD or memory error. Related large-menu scalability limits are already documented in the queue - see #191360 (the parent selector loading the whole tree) and #2862907 (saving large menus hitting PHP's max_input_vars).
The reordering interaction is also hard to operate with a keyboard or assistive technology: #3027229: Modernize tabledrag accessibility..
Steps to reproduce
- Install the Standard profile.
- Create a large number of links in a single menu (e.g.
main). Themenu_tree_ui_testsubmodule included in the MR ships a Drush seeder for this:drush mtu:seed maincreates ~15,000 links;drush mtu:seed main --counts=5000creates a wide single-level menu. - Visit
/admin/structure/menu/manage/main(themenu_uiedit form). - Observe a WSOD or an out-of-memory error, depending on PHP
memory_limitand environment settings. On menus large enough to render but not crash, observe that the page is slow to build.
Proposed resolution
Add a new experimental module, menu_tree_ui, built around lazy loading so the cost no longer scales with total link count, and intended to become the primary way people browse and reorder menus.

It does not remove menu_ui. When enabled, menu_tree_ui commandeers the primary "Edit menu" operation on /admin/structure/menu, and menu_ui's form is relegated to a fallback under a new secondary "Edit menu (no JS)" operation. menu_ui also continues to provide the per-link add/edit/delete forms, which menu_tree_ui reuses rather than reimplementing. So the tree UI becomes the primary interface, while the no-JS form stays available for environments or workflows that need it.
Key features:
- Loading is lazy and single-level:
MenuTreeLoader::loadLevel()keeps each fetch to one level (MenuTreeParameters::setRoot()->excludeRoot()->setMaxDepth(1)). Only the top level renders on page open; each subtree fetches on expand via a JSON endpoint. - Moves are committed through the existing menu link manager, so they persist to entities or
StaticMenuLinkOverridesexactly as core does today, and per-link edits delegate to the existing menu link form rather than being reimplemented. - The UI is a WAI-ARIA
treewith full keyboard navigation; pointer drag-and-drop that can reorder and reparent with a live drop indicator; and a keyboard equivalent - a grab/select move mode (Space to pick up a row, arrows to choose a before/after/child position, Enter to drop, Escape to cancel) that flows through the same drop-intent and commit path as drag, so the keyboard reaches exactly the destinations drag can. Every move is announced viaDrupal.announce(), and a "Keyboard commands" dialog (linked at the top of the page) documents the shortcuts. - An htmx-driven search panel offers jump-to-result, backed by a pluggable
MenuTreeSearchProviderInterface(the default provider does a titleLIKEquery overmenu_link_content). - A small column-plugin system (
MenuTreeColumnattribute + manager) surfaces per-link flags inline; the menu link "expanded" toggle ships as the default column. - The top level is server-rendered, so the real tree appears on first paint and a
<noscript>fallback links to the classic form. The UI is a<menu-tree>custom element rather than aDrupal.behaviorsimplementation, so it re-initialises automatically on htmx-swapped DOM, and the JavaScript is no-build classic JS, matching core's current convention. I did write it in esm to bundled first off and could revert to that if desired, but it came with a bunch of new dependencies like esbuild. - Moves are validated and transactional:
MenuTreeMover::move()guards against self-target and the 9-level depth cap, renumbers siblings, and writes parent + weight inside a DB transaction. Writes use header-based CSRF.
Out of scope, for follow-up:
An entity-agnostic approach to the current node edit widget that uses some of the internals in this proposal.
Remaining tasks
Honestly I'm not sure. I've been told that this section is meant to be for core gates so here's my take on the current status of each:
- Accessibility: the UI is a WAI-ARIA
treewith full keyboard operation including a grab/select move mode that mirrors drag (reorder + reparent), roving tabindex,Drupal.announce()output andprefers-reduced-motionhandling. The drag/keyboard equivalence is covered by a FunctionalJavascript test. It still needs an accessibility-maintainer review - WCAG 2.1 AA and colour contrast checks (WAVE), real screen-reader testing, and the new patterns tagged "needs accessibility review". - Performance: lazy single-level loading is the core premise, so this gate needs the evidence to back it - EXPLAIN output and index notes for the search
LIKEquery and the level load, and before/after profiling againstmenu_uion a large seeded menu to demonstrate the scaling claim. - Testing: kernel coverage exists for the loader, mover, column manager, search provider and controller; FunctionalJavascript tests cover drag-and-drop and the keyboard move mode. Remaining gaps are the search / jump-to and column-toggle paths (and the search-popover dismissal).
- Frontend: CSS follows BEM and JS uses data-attribute selectors, but the gate review needs to settle the deliberate departures from current convention (the
<menu-tree>custom element instead ofDrupal.behaviors, and the no-build classic JS versus the ESM/bundled approach noted above), plus linting and supported-browser checks. - Documentation: needs API docblocks audited on the public surface, a
menu_tree_ui.api.phpdocumenting the new hooks (hook_menu_tree_ui_level_alter,hook_menu_tree_ui_search_results_alter,hook_menu_tree_ui_row_operations_alter,hook_menu_tree_ui_pre_move_alter,hook_menu_tree_ui_post_move), help text, and a change record for the new operation and APIs. - Usability: the issue needs the "usability" tag, screenshots, and a UX review of taking over the primary "Edit menu" operation and relegating
menu_uito "Edit menu (no JS)".
But I think that might be all be a bit premature, let's start with some feedback? I showed this to a few people at DrupalSouth and they were pretty keen on it. @xjm encouraged me to make this a Major issue. I'm not tied to any of the decisions I made putting this together, I just did my best to solve the problem in a way that I think is most likely to be accepted into core.
There are a couple of architectural issues/trade-offs I'm aware of so far:
- The top level renders in Twig while lazily-loaded rows render from JSON in JS, so the two paths produce identical DOM and must be kept in sync. The children endpoint returns JSON deliberately, because the client needs structured data for the drag arithmetic. Could be avoided by rendering all rows in JS but I wanted a bit more than the error message to display when a non-js user arrives.
- There is no optimistic-concurrency token on writes: integrity currently rests on the structural guards, the DB transaction, and reading live state at commit time, so two admins editing the same menu simultaneously is unhandled, but that's current behaviour so I'm not all that concerned.
User interface changes
- When
menu_tree_uiis enabled, each menu on/admin/structure/menugains an "Edit menu" operation that opens the new tree UI; the existingmenu_uioperation is relabelled "Edit menu (no JS)" and remains available. - The new editing screen presents the menu as an expand/collapse tree that loads on demand, with a search box, optional inline column toggles, drag-and-drop and keyboard reordering, and a "Keyboard commands" dialog linked at the top of the page. No changes to the menu listing, the per-link add/edit forms, or the rendered front-end menus.
- No change for sites that do not enable the module.
Introduced terminology
- Menu tree UI - the experimental editing screen provided by this module.
- Level / subtree load - a single fetch of one parent's immediate children, the unit of lazy loading.
- Search provider - a tagged service implementing
MenuTreeSearchProviderInterfacethat supplies search results for a menu. - Column plugin - a plugin (
MenuTreeColumnattribute) that renders an extra per-row cell, optionally an interactive toggle. - Movability - whether a link can be reordered/reparented (content links and overridable default links can; other plugin-defined links are locked).
- Move mode - the keyboard grab/select interaction for moving a link (Space to pick up, arrows to position, Enter to drop, Escape to cancel).
| Comment | File | Size | Author |
|---|
Issue fork drupal-3593502
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #3
cilefen commentedThis seems to be part of #2742371: Drag and drop interfaces can break either browsers or exceed PHP server limits for forms. Do you agree?
Comment #4
darvanenI've been sitting on this since I started it in earnest at the DrupalSouth contribution day. I keep putting off sharing it and trying to make it perfect but I've reached the long tail and it needs feedback before working on that.
I was heavily assisted by AI in making this, though it is a design I've been thinking through for a long time and have previously built with a proprietary library. I haven't read every single line of code, but I am largely happy with the structure of the module.
I will go through it thoroughly after the idea has had some feedback.
Comment #5
darvanen@cilefen yes! I intended to make that a related issue, here we go.
Comment #6
darvanenHere are some screenshots, probably should have included these to begin with.
The view on first load:
The keyboard commands modal:
How it looks when you drill down a few levels:
A view mid-keyboard-move showing the item being moved and the drop location indicator (blue line), mouse is also supported but the visual isn't as easy to parse without actually doing it:
The search function including the quick edit links and keyboard cursor:
Tests are now passing.
Comment #7
darvanenComment #8
darvanenComment #9
godotislateOn top of getting general opinion, I think it might make sense to get a usability/accessibility review first before going too much further?
Comment #10
darvanenI didn't want to waste their time if the direction changed but I guess as it's a UI they're the most likely group to have reasons to change direction. Thanks for adding those tags, I'll follow up in the #ux channel.
Comment #11
godotislateYeah, it's a bit chicken and egg, but it probably makes more sense to have UX/a11y concerns already in mind before going further with development, instead of going further with development and finding out late that there are notable a11y issues that require refactoring to address.
Comment #12
darvanenComment #13
kentr commentedI think of it like general project management. Planning phase first to determine requirements, etc. Then start rough & iterate / drill down into the finishing touches.
Comment #14
benjifisherWe discussed this issue at #3592717: Drupal Usability Meeting 2026-06-12. That issue will have a link to a recording of the meeting.
The attendees at the usability meeting were @benjifisher, @darvanen, @rkoller, @simohell, and @worldlinemine. I am giving them credit on this issue.
If you want more feedback from the usability team, a good way to reach out is in the #ux channel in Slack.