Display one level only of a multi-level menu in Drupal

2022. Jun. 27. · 2 min read
Task: display the third level menu items of a four levels menu separately, without their children. Should be easy to set it using the admin UI of Drupal, right? Nowadays not, unfortunately!

The UI of a Drupal menu block provides options to set the "Initial visibility level" and the "Number of levels to display". So one should set the first to "3" and the second to "1" and its done. Sadly this is not true with the current, 9.4 version of Drupal when the first value is not "1" or "2".

I thought that there is a quick fix: turn to "off" the "Show as expanded" setting of the 3rd level menu items (and also turn to "off" the "Expand all menu links" setting in the menu block). However this does not work when you are on the page the menu item points to. (I mean: "Menu item X" points to "Page X". When you visit "Page X" then "Menu item X" will be expanded – this is the expected behavior of Drupal.)

There is a workaround!

While it is a back-end issue, there is a workaround that can be done in the frontend! Create a menu template name suggestion in a preprocess based on the name/id of the menu block. Then create a custom menu template that displays just one menu level.

I've found a discussion in the Drupal.org forum that helped me to create the functions below:

<?php

use Drupal\block\Entity\Block;

/**
 * Implements hook_preprocess_HOOK().
 */
function THEMENAME_preprocess_block(&$variables) {
  if (isset($variables['elements']['#id'])) {
    $block = $variables['elements']['#id'];
    if (!empty($block)) {
      $variables['content']['#attributes']['data-block'] = $block;
    }
  }
}

/**
 * Implements hook_theme_suggestions_HOOK_alter().
 */
function THEMENAME_theme_suggestions_menu_alter(array &$suggestions, array $variables) {
  if (isset($variables['attributes']['data-block'])) {
    $block_id = $variables['attributes']['data-block'];
    $suggestions[] = $variables['theme_hook_original'] . '__' . $block_id;
    $suggestions[] = 'menu__' . $block_id;
  }
}

Now we can create a new menu template with a name structure menu--name--blockname, like menu--main--3rd-level-nav.

In this template the only necessary change compared to the original menu macro is getting rid of the code related to the child items.

<li{{ item.attributes.addClass(classes) }}>
  {{ link(item.title, item.url) }}
  {# GET RID OF THIS "ITEM.BELOW" SECTION!
    {% if item.below %}
      {{ menus.menu_links(item.below, attributes, menu_level + 1) }}
    {% endif %}
  #}
</li>

And if you are a PHP developer you could check if you can fix the original Drupal core issue! And if you are not a PHP dev, you can encourage your PHP developer friends to do so! 🙂