Hi,

there is this nice <li{{ item.attributes }}> thing to set active class for menu items.
Now I have a structure, where I have e.g. /node/subnode. For /node the active class is set for the "node" menu item but for /node/subnode the "node" menu item should also have the active class set.

Thanks for hints

Comments

lokimoto’s picture

For those interested, here is a solution using the theme.theme file:

function theme_preprocess_menu(&$variables, $hook) {
  if ($hook == 'menu') {
    $current_path = \Drupal::request()->getRequestUri();
    $items = $variables['items'];

    foreach ($items as $key => $item) {
      // Set active to dom element if path of menu item matches current path
      if ($item['url']->toString() == $current_path) {
        // Add active link.
        $variables['items'][$key]['attributes']['class'][] = 'active';
      } else {
        // Set active to dom element if path of menu item matches first part of current path
        $url_fragments = explode('/', $current_path);
        if (count($url_fragments) > 1 AND '/' . $url_fragments[1] == $item['url']->toString()) {
          $variables['items'][$key]['attributes']['class'][] = 'active';
        }
      }
    }
  }
}
Rieffen’s picture

Thanks, that helped me.
I just changed
$variables['items'][$key]['attributes']['class'][] = 'active';
to
$variables['items'][$key]['attributes']->addClass('active');

leisurman’s picture

Will this only work if the menu id is "menu"? Is that what this does. if ($hook == 'menu'. I am outputting a li list of page title from a View, each are link to the content. How can I add a class active to this?

bradjones1’s picture

nanobyt3’s picture

Thanks, very helpful approach.
Works well for first level of menu.
I have modified it to work for entire depth of menu tree.

/**
* Implements hook_preprocess_menu() for menu.html.twig.
*/
function theme_preprocess_menu(&$variables, $hook) {
  if ($hook == 'menu__main') {
    $current_path = \Drupal::request()->getRequestUri();
    $items = $variables['items'];
    foreach ($items as $key => $item) {
      // Set active to dom element if path of menu item matches current path
      if ($item['in_active_trail']) {        
        // Add active-trail class
        $item['attributes']->addClass('active-trail');
        if ($item['url']->toString() == $current_path) {
          // Add active class
          $variables['items'][$key]['attributes']->addClass('active');
        } elseif (count($item['below'])) { // Process if sub-menu tree exists
          _menu_process_submenu($item['below'], $current_path);
        } else {
          // Set active to dom element if path of menu item matches first part of current path          
          $url_fragments = explode('/', $current_path);
          if (count($url_fragments) > 1 AND '/' . $url_fragments[1] == $item['url']->toString()) {
            $variables['items'][$key]['attributes']->addClass('active');
          }
        }
      }
    }
  }
}

/**
 * Set active and active-trail class for sub-menus recursively.
 */
function _menu_process_submenu(&$submenu, $current_path) {
  foreach ($submenu as $item) {
    if ($item['in_active_trail']) {
      // Add active-trail class
      $item['attributes']->addClass('active-trail');
      if ($item['url']->toString() == $current_path) {
        // Add active class
        $item['attributes']->addClass('active');
      } elseif (count($item['below'])) { // Process if sub-menu tree exists
        _menu_process_submenu($item['below'], $current_path);
      }
    }
  }
}
joos’s picture

themename.theme

/**
* Implements hook_preprocess_menu() for menu.html.twig.
*/
function themename_preprocess_menu(&$variables, $hook) {
  if ($hook == 'menu') {
    $current_path = \Drupal::request()->getRequestUri();
    foreach ($variables['items'] as &$item) {
      if ($item['in_active_trail']) {        
        if ($item['url']->toString() == $current_path) {
          $item['is_active'] = TRUE;
        } elseif (count($item['below'])) { 
          _themename_menu_process_submenu($item['below'], $current_path);
        } 
      }
    }
  }
}

/**
 * Set active and active-trail class for sub-menus recursively.
 */
function _themename_menu_process_submenu(&$submenu, $current_path) {
  foreach ($submenu as &$item) {
    if ($item['in_active_trail']) {
      if ($item['url']->toString() == $current_path) {
        $item['is_active'] = TRUE;
      } elseif (count($item['below'])) { 
        _themename_menu_process_submenu($item['below'], $current_path);
      }
    }
  }
}

menu--main.html.twig

...
{%
        set item_classes = [
          'dropdown',
          item.in_active_trail ? 'active-trail',
          item.is_active ? 'active',
        ]
%}
...
johnnny83’s picture

How would I have to change this, if I want to mark a specific menu trail active (1st and 2nd level) when a specific content type is opened?

joos’s picture

You could use css to target the specific content types (classnames on bodytag) and set styles to active-trail on level 1 & 2. Thats how I would do it.

Or, perhaps the question is about expanding the menu on a certain content type? In D7 I think I used the context module for that but I dont know if its ready for d8 yet.

On sites I build nowadays I always install "menu_block" (the extra module for D8) and make the primary navigation expanded (expand all menu links). Doing so I always have the complete menu tree expanded at the top of the page, for Google to index, and for me to style as I see fit. For instance I can make the menu collapsable on mobile devices without page reload. This is the latest example I've built (based on twitter bootstrap) http://www.radiohjalpen.se/.

johnnny83’s picture

Yes, that's what I want to do, expand the menu to a specific menu item on 2nd level when opening articles and mark it as active. I found this module https://www.drupal.org/project/context_active_trail but after reading your solution I was wonderung if I can do this without installing two extra modules.

joos’s picture

Yes, that's a problem one recognizes. Haven't found a solution for that in D8 though, but it's the context-module we're lacking. Sofar I've created several menu-blocks to expand a certain subtree for a content type, but ist a bad workaround, i think. Maybee this guy is onto something? https://www.dunix-data.de/blog/drupal_8_set_menu_active_trail_based_on_t...

johnnny83’s picture

Well, the idea with the different menu blocks is not bad! And how do you mark a specific menu item as active?

joos’s picture

There is no good way to do that (as I've figured out anyway). Maybe you could make a special theme template for that menu block and do it in twig. Or, you could style (css) a fixed menu-option as "active" on article-nodes.

youni’s picture

Your code works fine. But i have multilingual site and Main menu links are created with Taxonomy Menu missile. Your code works only with twig cache disabled while i debug my theme.

But when I enable twig cache and clear cache, this code affects only first menu item I visited. And when I open other items and their nodes, 'active' is that first element I visited first time. I think result of this code is cached and is always using.

Is there solution set 'active' class to main menu item for it works even with Twig cache enabled?

nickBumgarner’s picture

This appears to work really quite well. I'm curious if there is an alternate version of this which would work on the <li> instead of the <a> tags on the page. Having the class on the <li> would make it more flexible when trying to target sub-menu's of an active item.

Any help would be greatly appreciated here.

Nicholas A. Bumgarner
Web Developer
Inclind, Inc.

nickBumgarner’s picture

I was actually able to get this working by simply grabbing the menu.html.twig code from the Drupal 8 Core theme "Classy".

{#
/**
 * @file
 * Theme override to display a menu.
 *
 * Available variables:
 * - menu_name: The machine name of the menu.
 * - items: A nested list of menu items. Each menu item contains:
 *   - attributes: HTML attributes for the menu item.
 *   - below: The menu item child items.
 *   - title: The menu link title.
 *   - url: The menu link url, instance of \Drupal\Core\Url
 *   - localized_options: Menu link localized options.
 *   - is_expanded: TRUE if the link has visible children within the current
 *     menu tree.
 *   - is_collapsed: TRUE if the link has children within the current menu tree
 *     that are not currently visible.
 *   - in_active_trail: TRUE if the link is in the active trail.
 */
#}
{% import _self as menus %}

{#
  We call a macro which calls itself to render the full tree.
  @see http://twig.sensiolabs.org/doc/tags/macro.html
#}
{{ menus.menu_links(items, attributes, 0) }}

{% macro menu_links(items, attributes, menu_level) %}
  {% import _self as menus %}
  {% if items %}
    {% if menu_level == 0 %}
      <ul{{ attributes.addClass('menu') }}>
    {% else %}
      <ul class="menu">
    {% endif %}
    {% for item in items %}
      {%
        set classes = [
          'menu-item',
          item.is_expanded ? 'menu-item--expanded',
          item.is_collapsed ? 'menu-item--collapsed',
          item.in_active_trail ? 'menu-item--active-trail',
        ]
      %}
      <li{{ item.attributes.addClass(classes) }}>
        {{ link(item.title, item.url) }}
        {% if item.below %}
          {{ menus.menu_links(item.below, attributes, menu_level + 1) }}
        {% endif %}
      </li>
    {% endfor %}
    </ul>
  {% endif %}
{% endmacro %}

Nicholas A. Bumgarner
Web Developer
Inclind, Inc.

Ralf Eisler’s picture

Cool, this is exactly what I was looking for.

dampez’s picture

Nicholas

where did you placed the file? Sorry the ignorance.

Did you place it in the theme/template folder and named it like the machine name of the menu? menu--menuname.html.twig ?

Thanks for your help, I cant get it to work so far.

nickBumgarner’s picture

I placed the file in my theme's templates directory. The path is themes/YOURHTEME/templates/menu.html.twig

Make sure you clear your caches!

Nicholas A. Bumgarner
Web Developer
Inclind, Inc.

priyanka12chavan’s picture

I have installed link attributes module and adding classes from Menu UI. Need to apply those classes to li. with li{{ item.attributes }} nothing appears in li tag. Can someone help.

ihagen’s picture

/**
* Implements hook_preprocess_menu() for menu.html.twig.
*/
function themename_preprocess_menu(&$variables, $hook) {
  if ($hook == 'menu__main') {
    _themename_menu_process_menu($variables['items']);
  }
}

/**
 * Set active class for menus recursively.
 */
function _themename_menu_process_menu(&$menu) {
  $active_trail = False;
  foreach ($menu as &$item) {
    if ($item['in_active_trail']) {
      if (count($item['below'])) {
        $active_sub = _themename_menu_process_menu($item['below']);
      } else {
        $active_sub = False;
      }
      if (! $active_sub) {
        $item['is_active'] = TRUE;
      }
      $active_trail = True;
    }
  }
  return $active_trail;
}