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:
- Build a multi-level menu component in Pattern Lab
- Create a Twig template suggestion for the main menu
- Update Drupal’s menu macro to match our component’s markup
- Integrate the Main Menu component with Drupal’s Menu
Build a multi-level menu component in Pattern Lab
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 the component data or dummy content
- .css or scss for the component styles
- In some cases you may also need a Javascript file
For more in-depth explanation on building and integrating components read our blog series.
Note: This series is over two years old and many things have changed since then, but the basics of building a component still apply.
Building the component
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.
- In your theme’s components location create a new directory called main-menu
- In the main-menu directory, create the following three files:
- main-menu.twig
- main-menu.yml
- main-menu.scss
- Inside main-menu.yml add the code below:
- The data above represents an array of items. Each item has 3 key/value pairs: Title, URL and menu_level. Each item represents a link in the menu. 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.
- Inside main-menu.twig add the code below:
- Finally add the following styles in main-menu.scss
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:
It’s important to note that the component will simply be used for styleguide purposes. We will create a similar component for Drupal, but we will use the original component above as a base for markup styles and behavior.
Why do we need to create a duplicate component for Drupal?
Drupal provides attributes Pattern Lab does not understand. Rather than over complicating the component, we are going to keep it simple for styleguide purposes while the Drupal version is more elaborate with all the things Drupal needs. This is not required but provides more clarity to the process.
Create a Twig template suggestion for the main menu
Our ultimate goal is to update Drupal’s menu system to render with similar markup as the main-menu component. This will require several steps which we will cover next.
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:
Under File Name Suggestions notice two template names: menu.html.twig & main-menu.html.twig. The X next to menu.html.twig indicates Drupal is using this file to render the Main Menu. The second file, main-menu.html.twig, is what Drupal is suggesting we create if we only want to alter the Main Menu and not other menus.
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:
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).
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.Next, In your theme rename the newly copied template to main-menu.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 (main-menu.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).After clearing Drupal’s cache, inspect the menu again and you should see the X next to main-menu.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.
Customize the template suggestion
Before we look at the code inside menu.html.twig as it may look confusing or not familiar, we are going to move it into the same directory as the main-menu component. We are doing this to make some improvements to the macro.
In the same directory where you created the main-menu component, create a new file called _main-menu-macro.twig (notice the underscore as first character of file name). This file is only for Drupal purposes and we don’t need it to be shown in Pattern Lab. The underscore allows Pattern Lab to ignore it.
Copy all the code from main-menu.html.twig into _main-menu-macro.twig.
The above code is a Twig Macro. Macros are the Twig version of functions. Just like you would write a function in PHP to do something and return something, you can use a Twig macro to generate some output.
Rendering menu with default macro
If we were to use the macro provided by Drupal as is our menu would render at its simplest form with minimum markup and almost no semantic css classes. See example:
The markup above may look different depending on your base menu. The classy base menu may show more markup but it does not make our job easier to style the menu.
Update Drupal’s menu macro to match our component’s 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 place the macro within the same twig template from which you call it. The context is used in this case (line 21). 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 (main_menu), (line 23), 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, main_menu,( line 25), and pass the slightly modified parameters items, attributes, menu_level. We've changed the menu_level to a variable. If you are wondering where these parameters come from, the comments above the macro code in menu.html.twig provide all these variables.
- Now that we’ve updated the arguments, we self import the macro again to make use of it (line 26).
- Before writing any markup, we first check whether there are menu items to render (line 27).
- 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 class.
- 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 (line 33), and for each item, we print a <li> some classes which can be helpful if we need to style the different states of the menu.
- For each link in a list item, we pass two things:
- The node title
- The node url
- Inside the list item, we check to see if that particular item has other items or submenus.
- 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:
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 Main Menu is rendered with the markup, styles and behavior we implemented when we built the component.
- >In your editor, open [site_root]/themes/custom/<your-theme>/templates/navigation/main-menu.html.twig
- Delete all the content in the twig template except for the comments, and then paste the code below into it
{{ attach_library('your_theme/main-menu') }} {% import '@patterns/main-menu/_main-menu-macro.twig' as menus %} {{ menus.main_menu(items, attributes, 0) }}
- Clear Drupal’s cache
- Reload Drupal’s page
Since we moved the macro to the component’s location, all we need to do in the main-menu.html.twig template is to import the macro and provide the parameters the macro expects. These parameters can be found in the twig template’s comments.
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.
IMPORTANT: If you are using the Main Navigation blog to position the navigation where you want it in the page, you may need to make a copy of the twig template for that block in order to assign the main-menu class the menu needs.
- Copy block--system-menu-block.html from your base theme into your own theme’s /templates directory
- Modify the template so it includes the main-menu class, as shown below:
<nav class="main-menu" role="navigation" aria-labelledby="{{ heading_id }}"{{ attributes|without('role', 'aria-labelledby') }}>
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.
In our case our library would look like this:
Inside /themes/custom/<your-theme>/your-theme.libraries.yml add the following code
main-menu: css: component: dist/css/main-menu.css: {}
This library has already been attached above inside menu--main.html.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.
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 helped create 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.