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

  1. Install the Standard profile.
  2. Create a large number of links in a single menu (e.g. main). The menu_tree_ui_test submodule included in the MR ships a Drush seeder for this: drush mtu:seed main creates ~15,000 links; drush mtu:seed main --counts=5000 creates a wide single-level menu.
  3. Visit /admin/structure/menu/manage/main (the menu_ui edit form).
  4. Observe a WSOD or an out-of-memory error, depending on PHP memory_limit and 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.

screenshot of proposed menu UI

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 StaticMenuLinkOverrides exactly 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 tree with 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 via Drupal.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 title LIKE query over menu_link_content).
  • A small column-plugin system (MenuTreeColumn attribute + 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 a Drupal.behaviors implementation, 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 tree with full keyboard operation including a grab/select move mode that mirrors drag (reorder + reparent), roving tabindex, Drupal.announce() output and prefers-reduced-motion handling. 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 LIKE query and the level load, and before/after profiling against menu_ui on 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 of Drupal.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.php documenting 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_ui to "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_ui is enabled, each menu on /admin/structure/menu gains an "Edit menu" operation that opens the new tree UI; the existing menu_ui operation 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 MenuTreeSearchProviderInterface that supplies search results for a menu.
  • Column plugin - a plugin (MenuTreeColumn attribute) 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).

Issue fork drupal-3593502

Command icon 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

darvanen created an issue. See original summary.

cilefen’s picture

darvanen’s picture

I'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.

darvanen’s picture

@cilefen yes! I intended to make that a related issue, here we go.

darvanen’s picture

StatusFileSize
new320.13 KB
new298.79 KB
new327.29 KB
new354.59 KB
new383.19 KB

Here are some screenshots, probably should have included these to begin with.

The view on first load:

full menu tree ui dashboard on load

The keyboard commands modal:

screenshot of keyboard commands modal

How it looks when you drill down a few levels:

screenshot showing the UI five levels deep

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:

screenshot of the ui mid-keyboard-move

The search function including the quick edit links and keyboard cursor:

screenshot showing the search function

Tests are now passing.

darvanen’s picture

Title: Menu UI runs out of memory » New Menu UI / menu_ui runs out of memory
darvanen’s picture

Issue summary: View changes
godotislate’s picture

On top of getting general opinion, I think it might make sense to get a usability/accessibility review first before going too much further?

darvanen’s picture

I 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.

godotislate’s picture

Yeah, 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.

darvanen’s picture

Issue summary: View changes
kentr’s picture

Issue tags: +Usability, +Accessibility

I think of it like general project management. Planning phase first to determine requirements, etc. Then start rough & iterate / drill down into the finishing touches.

benjifisher’s picture

We 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.

idebr’s picture

This is essentially the problem being solved in https://www.drupal.org/project/bigmenu

It might be an idea to develop the module as a contrib module to allow quicker iteration

darvanen’s picture

Big Menu doesn't use ajax any more.

I was encouraged by @xjm - a core committer - to make this a core issue rather than contrib.

smustgrave’s picture

darvanen’s picture

@smustgrave yeah probably, I didn't even have those fields at the top of the prototype.

benjifisher’s picture

I can summarize the Usability meeting (see Comment #14): the overall approach is great, but try to keep the good parts of what you are replacing as you move beyond proof of concept. Despite its flaws, the current menu UI is the result of a lot of thought and refinement. In particular:

  • Keep the <table>-based markup. It is important for a11y.
  • In fact, keep the markup changes to a minimum. That will make the change less disruptive, and and it will mean less CSS work.
  • Use the same icon for the "handles". (I do not want to redo the bikeshedding!)
  • Keep the Submit button. I do not want to spend half an hour making changes, realize that I did it all wrong, and NOT have a way to cancel my changes.
  • Keep the "Add child" link in the drop button.

I have a personal interest in the last point, since I worked on #3379293: Make it easier to add a child menu item. If the fix is out of scope for this issue (because the current implementation is in Drupal\menu_ui\MenuForm, and the new implementation bypasses that form class) then it can be a follow-up issue, and I will help with it.

Two more observations from the Usability meeting:

The Administration menu (in the Standard profile) has just one top-level item. On initial load, nothing is expanded. In this case, it would be nice to expand an extra level. Maybe also if there are 2 top-level items. I am not sure what the cutoff should be, but I think 5 is too high.

If you select an item for moving, then that selection should "stick" if you then search for where to put it.

smustgrave’s picture

Fyi I did close #2520232: Separate the menu settings from the 'add link' button so we could address here

btw LOVING the new UI

darvanen’s picture

Assigned: Unassigned » darvanen
Status: Active » Needs work

I've pushed an update but it's taking a while to show in the gitlab UI. Running an event this weekend so I can't really interact again until Monday. I'll try to give a sit-rep then.
Self-assigning because I do intend to continue working on this. If the MR updates please feel free to feedback on the experience. The code is in reasonable shape but I wouldn't do a fine-tooth-comb on it just yet.
#ai-assisted

benjifisher’s picture

Related issue: #3558415: Alternative token selection form element.

That issue and this one use HTMX with a similar strategy: expand one level of the tree at a time. We should think about how to avoid code duplication and how to make it easier for contrib and custom modules to take advantage of the work done here.

darvanen’s picture

That's bigger than it sounds because token browser mustn't have any of the reordering/relevelling behaviours required by the menu tree, but I agree, and it should be extended to taxonomy eventually - which takes us to the rule of three, kind of. I'm sure contrib will find other uses for it too...

So now I'm working on a Drupal TreeGrid as the base, with a Movability layer that would be used by menu and taxonomy but not tokens, and one consumer of those systems - the Menu UI

My only worry is that this thing is going to be so big it won't get merged.

darvanen’s picture

Assigned: darvanen » Unassigned
Status: Needs work » Needs review

Alrighty, have reached the point of fatigue on this one at the moment. I'm sure there's more to do but I don't know what it is, time for a round of feedback :)

There are now three modules.

  • treegrid: generic element, lazy one-level-at-a-time loading, row windowing, WAI-ARIA keyboard nav with a roving tabindex, and a column-plugin system.
  • treegrid_reorder: drag-and-drop + keyboard reordering on top of treegrid, gesture handling, move validation, per-user Save/Cancel working copy.
  • menu_tree_ui: the menu-specific consumer, row renderer, Enabled/Operations column plugins, and the move handler.

menu_ui now splits the menu admin into two tabs: Edit (settings) and Links (the link list, at …/manage/{menu}/links), with menu_tree_ui providing the JS tree as the Links experience and the classic overview kept as the no-JS fallback.

@rkoller if you see this, I chose to try keeping the arrow keys for navigation and tab for getting to the row internals with better voice-prompts. This way tabbing doesn't get trapped inside all the many rows first visible on load, and navigation remains easy to use with arrow keys for up/down and open/close. I couldn't make tabbing between rows AND tabbing along a row work. Happy to try other approaches but would need a more specific model.

darvanen’s picture

Title: New Menu UI / menu_ui runs out of memory » Introduce a new Menu UI to resolve memory issues and improve user experience
darvanen’s picture

Alright, I had access to Fable for a while so I threw everything I could at this (and then paused to think over it before posting). Here's what landed:

  • The move and children endpoints now check that every ID belongs to the route's menu. Bad input returns an error, not a crash.
  • Pending moves sit in a working copy until Save. A second editor sees who holds the lock and can break it. A partial save reports what saved.
  • Save applies moves one at a time, in order. Menu writes cannot run inside a database transaction.
  • Wide menus render the first 200 top-level links. The rest load as you scroll.
  • Keyboard fixes: keys inside a row are left alone, drops announce once, and screen readers hear rows by name.
  • Ordering logic is tested at the PHP level. Browser tests cover only what needs a browser.
  • The code is three modules: treegrid (the grid), treegrid_reorder (moving rows), and menu_tree_ui (the menu screen). The generic two never mention menus.

That last point is there to support using this code to power other UIs which I have now had requested in multiple places (some of them DMs):

  1. treegrid by itself would be used by something like the Token browser which has no need for reordering logic and shouldn't be forced to work around it.
  2. treegrid_reorder adds that functionality, which would also be used by, say, taxonomy. It depends on treegrid.
  3. menu_tree_ui depends on treegrid_reorder and layers on all the menu-specific functionality

It's a lot. I'm not asking for code-level reviews yet, I have to finish reading it all before I can meaningfully respond. I am asking for architectural opinions. Please?

darvanen’s picture

StatusFileSize
new60.45 KB
new26.42 KB

For the sake of transparency, I'm uploading the files I had the agent use to track work :)

darvanen’s picture

Considering the size this has grown to, I'm considering moving back to a contrib approach. I would make treegrid one module with treegrid_reorder as a submodule, and menu_tree_ui as a different contrib module listing those two as dependencies.

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new16.92 KB

The Needs Review Queue Bot tested this issue. It fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".

This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

darvanen’s picture

Status: Needs work » Needs review

Oh cool, twig linting!

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new91 bytes

The Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".

This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

darvanen’s picture

Status: Needs work » Needs review
Issue tags: +no-needs-review-bot

The two recent pipeline failures are unrelated to this MR.

Pipeline 934316 failed on CKEditor5AllowedTags::testFullHtml with "cannot call methods on dialog prior to initialization". That is the dialog resize race from #3472624: Ensure the UI dialog instance is valid in Drupal.dialog.resetSize, also reported in #3615749: Exclude ckeditor5.css from aggregation. The re-run passed that test.

Pipeline 934458 failed only on Nightwatch. The navigation module's expandCollapseTest timed out waiting for the admin toolbar listener element. This MR does not touch the navigation module. The same tree passed Nightwatch in pipeline 934316.

The PHPUnit Unit 8.6-ubuntu jobs fail on current main in every MR and are marked allow-failure.

No menu_ui, menu_tree_ui or treegrid test has failed in either pipeline. I cannot retry jobs on this project; a re-run of the Nightwatch job should clear the pipeline.

darvanen’s picture

Issue tags: -no-needs-review-bot

Removing the no-bot tag because the pipeline has now passed :)

If someone helped, thank you! If that was automatic... well, that's very cool.

mherchel’s picture

I'm so excited that this is happening!

One thing that you'll want to do is make sure it supports the new Admin theme's dark mode (currently it doesn't appear to).

I just wrote a documentation guide on how to do so at https://www.drupal.org/docs/core-modules-and-themes/core-themes/default-...

Take a look and let me know if it makes sense or you need help.

---

Leaving at NR since there's still tons of reviews that need to happen on this issue.

darvanen’s picture

Issue tags: +no-needs-review-bot

Thanks @mherchel, I think that last (code) commit should cover it.

Totally agree on leaving it as NR.
I'm slowly reading through the code but I'm very ready to defend the architecture.

Once again a known flake has turned the pipeline red, so I'm gonna add the bot exclusion tag.

darvanen’s picture

The rebase was to check if anything needed updating due to the HTMX 4 upgrade.