By nature, menus are complicated. They’re not easy to work with, build, or style, and yet they are probably the single most important navigation element of any website. Despite their complication, they need to be perfect to serve their purpose. A menu can make or break a website. They can make your browsing experience a pleasant one, or can drive you to leave a website frustrated and likely never to return.

When it comes to menus and forms, it is best to let Drupal dictate the direction rather than build them independently of Drupal. By adhering to Drupal’s best practices we can save a lot of time, effort, and frustration. For example, rather than writing your menu’s markup before you see it being printed by Drupal, it is best to build your menu in Drupal and see what the markup looks like. Then you can use Drupal’s markup as a starting point for writing your menu’s markup. 

This is going to be a lengthy post (mostly because it includes a lot of code snippets), but this article will cover all you need to accomplish the following:

 Editor's note: This blog was originally published on June 3, 2019, and has been updated to include two new tutorial videos. They can be found at the end of the post. 

Build a Multi-Level Menu Component in Pattern Lab

For the purpose of keeping the content clean, I'll be linking to code snippets from Github Gists.

Let’s start by building a pretty straight forward multi-level menu component in Pattern Lab. If you’ve used Pattern Lab or KSS Node you know most components require at least three files: 

  • .twig for the component markup and logic
  • .yml or json for demo links or content
  • .scss for the menu styles
  • In some cases you may also need a Javascript file

For a more in-depth explanation on building and integrating components read our blog series.  
Note: This series is over three years old and many things have changed since then, but the basics of building a component still apply.

Building the component

  1. In your theme’s components, location create a new directory called navigation
  2. In the navigation directory, create the following three files:
    • navigation.twig
    • navigation.json
    • navigation.scss
  3. Inside navigation.json add the code below:
    Menu data structure
  4. The data above represents an array of items. You may notice some of the links have nested arrays (i.e. below). These represent submenus in the menu tree. The items array above is only intended to simulate the menu’s data for our component. In Drupal, the array will be provided when we add menu links to Drupal’s Main Menu.
  5. Inside navigation.twig add the code below:
    Menu logic and markup
  6. If the code above does not look familiar to you it’s probably because you’ve never used a Twig Macro. I will explain the macro in detail shortly, but for now, let’s move on to completing the component.  I have also recorded a screencast on Twig Macros.
  7. Finally add the following styles in navigation.scss
    Menu CSS styles

Update Drupal’s Menu Macro to Achieve Better Markup

Not having the right markup for the menu, or any other element for that matter can really complicate things for us when it’s time to style. We are going to modify the macro so it matches the markup of the main-menu component, and as a result, the styles we wrote for the component will apply to the Drupal menu.

Menu Macro Explained

Let’s step through this macro line by line to understand what it does. To keep things simple we are going to ignore all the custom classes we added above and focus on the macro itself.

  • Twig macros can be called from other twig templates, and it's also possible to call them from within the same twig template where it was written (as is the case here by using the variable name _self). So, with this import statement, we imported the macro and set it as a variable called menus.
  • Using the menus variable we then assign a name to it (menu_links), which will take the following parameters: items, attributes, and 0). This name can be anything you wish.
  • Now we declare the macro again but this time with its new name, menu_links, and pass the slightly modified parameters items, attributes, menu_level
  • Now that we’ve updated the arguments, we self import the macro again to make use of it 
  • Before writing any markup, we first check whether there are menu items to render
    • If there are items in our main menu we start writing the markup. However, since we may have a multi-level menu, we want to make sure we identify the top level menu from nested (submenus), menus in the tree.  
    • This is where we first check if the menu_level is 0 (zero).
    • If it is the top level menu, we print a <ul> and we pass Drupal’s attributes in addition to our custom classes.
    • If menu is not top level (submenus), then we simply print a <ul> with its corresponding class.

Learn more about Macros in Twig Templates.

Looping Through the Menu Tree

Looping through the items array allows us to intersect each item and apply classes or attributes.

  • First, we loop through the items array (for item in items), and for each item, we print a <li> to which we pass an array of CSS classes using attributes.addClass() method.  However we do this only if we have access to Drupal's attributes ({% if attributes %}).  If not, we manually print the same classes on the list item. 
  • For each item and link, we pass a series of things such as:
    • The link title
    • The link url
    • Is the link in the active trail,
    • ... and others.
  • Inside the list item, we check to see if that particular item has other items or submenus({% if item.below %})
  • When submenus exist, we make use of the macro again to repeat the process of printing the menu. This is where the macro saves us from writing duplicate code.
  • For each level of submenus we are increasing the menu_level by 1 and printing it as part of the submenu class. This is handy if we want to target a specific submenu in the tree.
  • Finally, we close all previously open actions/tags (i.e. for loops, if statements, lists, and macro).

RESOURCE: I recently ran into a blog post by Tamas Hajas where he has a great way to address classes and states on a menu. Check it out: Drupal 8 Twig: add custom CSS classes to menus

Looking at the Rendered Menu after Macro Improvements

Now that we have a better understanding on how the macro works, and after making the improvements discussed above, the markup for the menu would look something like this:
Example of menu with good markup.

 

These styles are as bare as possible. They are simply to make the menu look presentable and could use a lot of improvements. This is what the multi-level menu looks like:

Example of an expanded menu

 

Create a Twig Template Suggestion for the Main Menu

Our ultimate goal is to update Drupal’s menu system to render using the navigation component. 

Follow these steps to enable Twig Debugging in your theme. Then, you can inspect your site, which will allow you to see the various template suggestions that Drupal uses to render the page; including your navigation menu. The debug information looks like this:

Example of twig debugging code.

Under File Name Suggestions notice two template names: menu.html.twig & menu--main.html.twig. The X next to menu.html.twig indicates Drupal is using this file to render the Main Menu. The second file, menu--main.html.twig, is what Drupal is suggesting we create if we only want to alter the Main navigation and not other menus.  The word "main" in the file name represents the Drupal machine name for that menu.

Under Begin Output notice the path where menu.html.twig can be found, the example above is pointing to Drupal’s stable theme.

To ensure we only affect the main menu and not other menus on our site, we are going to make a copy of Drupal’s menu.html.twig in our theme and then override it. This is recommended rather than modifying Drupal core’s template. Let’s start:

  1. Copy the menu.html.twig template from core/themes/stable/templates/navigation/ into [site_root]/themes/custom/<your-theme>/templates/navigation/menu.html.twig (if these folders do not exist yet in your theme go ahead and create them).
  2. Following the golden rule “Never hack core”, we want to make a copy of Drupal’s menu template in our own theme. Replace the core theme’s name with your core base theme if you are not using stable.
  3. Next, In your theme rename the newly copied template to menu--main.html.twig.
    Copying menu.html.twig into our theme will affect other menus. This is why we are renaming the template so it only affects the main menu (menu--main.html.twig). Replace ‘main’ with whatever your menu’s machine name is if you are not using Drupal’s Main Menu. This can be found in Drupal’s Menus page (admin/structure/menus).
    ,>
  4. After clearing Drupal’s cache, inspect the menu again and you should see the X next to menu--main.html.twig which means Drupal is now using our custom twig template suggestion to render the menu.

Create a Multi-level Menu in Drupal

  • Let’s make sure we have a menu we can see in Drupal. Let’s create a multi-level menu using the Main Navigation menu (Structure | Menus | Main Navigation).
  • Add as many links as you wish. Be sure to add nested items so we end up with a multi-level menu.
  • In addition, ensure each of the submenu’s parent links are set to “show expanded”. You will see the option on the Add/Edit Link page.
  • Finally, go to Structure | Block Layout and click Configure next to Main Navigation
    • Change Number of levels to display to Unlimited. This will ensure dropdowns are rendered and display in your navigation.

Integrate the Main Menu component with Drupal’s Menu

The last part of the process is to integrate all the work we’ve done with Drupal, so our Navigation is rendered with the markup, styles, and behavior we implemented when we built the component.  

  1. In your editor, open [site_root]/themes/custom/<your-theme>/templates/navigation/menu--main.html.twig
  2. Delete all the content in the twig template except for the comments, and then paste the code below into it
{% include '@your-namespace-here/navigation/navigation.twig' %}
  1. Clear Drupal’s cache
  2. Reload Drupal’s page

Since we moved the macro to the component’s location, all we need to do in the menu--main.html.twig template is to call our Navigation component by using a Twig include statement. 

If we did our job right, Drupal’s menu should now look and behave the same way as our component. In addition, if you inspect the Drupal page, the menu should reflect the same markup as the main-menu component.

Drupal Libraries

As a best practice, we should create a drupal library to attach any styles and javascript to our component. See this article for adding CSS and JS to a page or component.

If your navigation is displaying unstyled that's because a Drupal library does not exist.  Create one as follows:

  • Inside /themes/custom/<your-theme>/your-theme.libraries.yml add the following code

navigation:
  css:
    component:
      dist/css/navigation.css: {}

Your compiled css path may be different.

This library has already been attached above inside navigation.twig

By doing this any styles we wrote for the component will apply to the main navigation when it’s rendered in Drupal. Drupal libraries have no effect in Pattern Lab.

After completing the steps above, clear your Drupal’s cache and reload your Drupal site. You should see your Drupal menu being rendered with all the styles and markup we wrote throughout this post,

What about Accessibility?

You should always ensure your menus and navigations are accessible. It is the right thing to do. However, don't yell at me for not following my own advice. In an effort to keeping this tutorial simple, I opted to omit anything related to accessibility. Perhaps part two can focus on that ;-)

Video Tutorials


In Closing

I’d like to admit that I went about the long way to get the main menu working on both, Pattern Lab and Drupal. There are other ways to accomplish this by using contrib modules such as Simplify Menu (full disclosure, I am a co-maintainer of the module), and perhaps Twig Tweak among several others, however, the approach I took here gives you the most control and that can make all the difference if you are dealing with a pretty advanced or complicated menu.